mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-22 18:42:52 +00:00
Compare commits
149 Commits
clock-date
...
20260325.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9b026d0ef | ||
|
|
35339906ec | ||
|
|
ce23f716cc | ||
|
|
aaf8fa199f | ||
|
|
fba430d507 | ||
|
|
59361cbd38 | ||
|
|
b558117d8c | ||
|
|
a7c8347751 | ||
|
|
31ca9c849a | ||
|
|
6252d7e8f5 | ||
|
|
f42986adf6 | ||
|
|
9e70ea3723 | ||
|
|
de3b7bf513 | ||
|
|
2c5f491c9e | ||
|
|
1ef13c5100 | ||
|
|
c166335aca | ||
|
|
c64ec21eca | ||
|
|
8d62056f4a | ||
|
|
62e73608b6 | ||
|
|
aa66d8891c | ||
|
|
494a96c635 | ||
|
|
36d77f54ce | ||
|
|
12fec9f580 | ||
|
|
5f1f55448a | ||
|
|
837e345ecf | ||
|
|
0929d7d18a | ||
|
|
70991d3c1e | ||
|
|
82e5bd62a1 | ||
|
|
b8adf4e866 | ||
|
|
111be984e0 | ||
|
|
78a2cbb532 | ||
|
|
34b09b140b | ||
|
|
f173f901c5 | ||
|
|
ebb6ac8d8b | ||
|
|
abe214a33a | ||
|
|
248332ae27 | ||
|
|
82fc2fccdc | ||
|
|
c8f30a7ee4 | ||
|
|
77f48d91cd | ||
|
|
caa707a7b1 | ||
|
|
0bed0fa37e | ||
|
|
5b6309d984 | ||
|
|
264818bc70 | ||
|
|
d664ab6836 | ||
|
|
a6c4184054 | ||
|
|
cb6985eb7c | ||
|
|
d466ab63bd | ||
|
|
1132cdb364 | ||
|
|
0f9d48a03d | ||
|
|
7e085d9b08 | ||
|
|
1a62c7296c | ||
|
|
be1921229c | ||
|
|
640558ad35 | ||
|
|
99636c9719 | ||
|
|
87758cc228 | ||
|
|
60e8b8b505 | ||
|
|
3c012c30ac | ||
|
|
84d234a330 | ||
|
|
a12543fe74 | ||
|
|
cc53f977a2 | ||
|
|
71541625d7 | ||
|
|
43da700ccc | ||
|
|
efbbdbf3e8 | ||
|
|
eee6f79639 | ||
|
|
9381bbd656 | ||
|
|
2724087290 | ||
|
|
6bdf1ccd8c | ||
|
|
79743c0afa | ||
|
|
15b1df5a58 | ||
|
|
8222d9796c | ||
|
|
3337b414d7 | ||
|
|
ce90d83c92 | ||
|
|
1f6d0d2e63 | ||
|
|
0327b02d0b | ||
|
|
780db9b066 | ||
|
|
89755f274d | ||
|
|
6ea15f507a | ||
|
|
c506fa8990 | ||
|
|
9470863808 | ||
|
|
4bdac1f385 | ||
|
|
a2a38e1da7 | ||
|
|
88c063ba2a | ||
|
|
eb8b2a9d17 | ||
|
|
10e8c2a148 | ||
|
|
e1a8616ab0 | ||
|
|
ccdd71dd64 | ||
|
|
d3e1d55686 | ||
|
|
4f916abcbf | ||
|
|
4548f9daae | ||
|
|
4020bcec42 | ||
|
|
22c0035e60 | ||
|
|
6b6ad8dd2c | ||
|
|
9c4aacdb1f | ||
|
|
3feb40a8f4 | ||
|
|
7a310812e0 | ||
|
|
ee77619da3 | ||
|
|
cfa8eb5370 | ||
|
|
d9d2d6aa03 | ||
|
|
1f46f477c7 | ||
|
|
52667b3266 | ||
|
|
c790d2356c | ||
|
|
f24c009dd7 | ||
|
|
8d42395938 | ||
|
|
1a6d46a7ff | ||
|
|
b286b07cfd | ||
|
|
1859d35f7b | ||
|
|
5709af57de | ||
|
|
bb16cc8c00 | ||
|
|
17c6dc52a8 | ||
|
|
1b8211db6d | ||
|
|
2b2bb77a2b | ||
|
|
64749350ef | ||
|
|
043d4eed85 | ||
|
|
2f2e64bb1d | ||
|
|
b74b02c09f | ||
|
|
ab4c3a4316 | ||
|
|
15de137591 | ||
|
|
465c10b945 | ||
|
|
457c51cf58 | ||
|
|
640f2b9245 | ||
|
|
852caa32be | ||
|
|
67ccfa0f6e | ||
|
|
c3cc566fe3 | ||
|
|
38d02a3f30 | ||
|
|
ad1d1e2260 | ||
|
|
b2eb8ec968 | ||
|
|
7b8884f0fd | ||
|
|
aff1fedc9d | ||
|
|
8f5059c24a | ||
|
|
1e72ad1411 | ||
|
|
c9f96bbe69 | ||
|
|
616c3d4657 | ||
|
|
b1ceece224 | ||
|
|
d695c4c845 | ||
|
|
fdbeb12622 | ||
|
|
29ede122a1 | ||
|
|
519d3d0e53 | ||
|
|
030a9a492c | ||
|
|
2685a007e7 | ||
|
|
9ca1cfbf4a | ||
|
|
0793af6846 | ||
|
|
bb7f441d8d | ||
|
|
2813ed7938 | ||
|
|
9ebfa4029b | ||
|
|
6190ba18ea | ||
|
|
81feea1109 | ||
|
|
be430931cc | ||
|
|
e07194027a | ||
|
|
17d9cd192f |
@@ -99,6 +99,41 @@ 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
|
||||
);
|
||||
|
||||
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 +153,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;
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
82
gallery/src/pages/components/ha-input.markdown
Normal file
82
gallery/src/pages/components/ha-input.markdown
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Input
|
||||
---
|
||||
|
||||
# Input `<ha-input>`
|
||||
|
||||
A text input component supporting Home Assistant theming and validation, based on webawesome input.
|
||||
Supports multiple input types including text, number, password, email, search, and more.
|
||||
|
||||
## Implementation
|
||||
|
||||
### Example usage
|
||||
|
||||
```html
|
||||
<ha-input label="Name" value="Hello"></ha-input>
|
||||
|
||||
<ha-input label="Email" type="email" placeholder="you@example.com"></ha-input>
|
||||
|
||||
<ha-input label="Password" type="password" password-toggle></ha-input>
|
||||
|
||||
<ha-input label="Required" required></ha-input>
|
||||
|
||||
<ha-input label="Disabled" disabled value="Can't touch this"></ha-input>
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
This component is based on the webawesome input component.
|
||||
|
||||
**Slots**
|
||||
|
||||
- `start`: Content placed before the input (usually for icons or prefixes).
|
||||
- `end`: Content placed after the input (usually for icons or suffixes).
|
||||
- `label`: Custom label content. Overrides the `label` property.
|
||||
- `hint`: Custom hint content. Overrides the `hint` property.
|
||||
- `clear-icon`: Custom clear icon.
|
||||
- `show-password-icon`: Custom show password icon.
|
||||
- `hide-password-icon`: Custom hide password icon.
|
||||
|
||||
**Properties/Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| appearance | "material"/"outlined" | "material" | Sets the input appearance style. "material" is the default filled style, "outlined" uses a bordered style. |
|
||||
| type | "text"/"number"/"password"/"email"/"search"/"tel"/"url"/"date"/"datetime-local"/"time"/"color" | "text" | Sets the input type. |
|
||||
| value | String | - | The current value of the input. |
|
||||
| label | String | "" | The input's label text. |
|
||||
| hint | String | "" | The input's hint/helper text. |
|
||||
| placeholder | String | "" | Placeholder text shown when the input is empty. |
|
||||
| with-clear | Boolean | false | Adds a clear button when the input is not empty. |
|
||||
| readonly | Boolean | false | Makes the input readonly. |
|
||||
| disabled | Boolean | false | Disables the input and prevents user interaction. |
|
||||
| required | Boolean | false | Makes the input a required field. |
|
||||
| password-toggle | Boolean | false | Adds a button to toggle the password visibility. |
|
||||
| without-spin-buttons | Boolean | false | Hides the browser's built-in spin buttons for number inputs. |
|
||||
| auto-validate | Boolean | false | Validates the input on blur instead of on form submit. |
|
||||
| invalid | Boolean | false | Marks the input as invalid. |
|
||||
| inset-label | Boolean | false | Uses an inset label style where the label stays inside the input. |
|
||||
| validation-message | String | "" | Custom validation message shown when the input is invalid. |
|
||||
| pattern | String | - | A regular expression pattern to validate input against. |
|
||||
| minlength | Number | - | The minimum length of input that will be considered valid. |
|
||||
| maxlength | Number | - | The maximum length of input that will be considered valid. |
|
||||
| min | Number/String | - | The input's minimum value. Only applies to date and number input types. |
|
||||
| max | Number/String | - | The input's maximum value. Only applies to date and number input types. |
|
||||
| step | Number/"any" | - | Specifies the granularity that the value must adhere to. |
|
||||
|
||||
**CSS Custom Properties**
|
||||
|
||||
- `--ha-input-padding-top` - Padding above the input.
|
||||
- `--ha-input-padding-bottom` - Padding below the input. Defaults to `var(--ha-space-2)`.
|
||||
- `--ha-input-text-align` - Text alignment of the input. Defaults to `start`.
|
||||
- `--ha-input-required-marker` - The marker shown after the label for required fields. Defaults to `"*"`.
|
||||
|
||||
---
|
||||
|
||||
## Derivatives
|
||||
|
||||
The following components extend or wrap `ha-input` for specific use cases:
|
||||
|
||||
- **`<ha-input-search>`** — A pre-configured search input with a magnify icon, clear button, and localized "Search" placeholder. Extends `ha-input`.
|
||||
- **`<ha-input-copy>`** — A read-only input with a copy-to-clipboard button. Supports optional value masking with a reveal toggle.
|
||||
- **`<ha-input-multi>`** — A dynamic list of text inputs for managing arrays of strings. Supports adding, removing, and drag-and-drop reordering.
|
||||
232
gallery/src/pages/components/ha-input.ts
Normal file
232
gallery/src/pages/components/ha-input.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { mdiMagnify } from "@mdi/js";
|
||||
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-svg-icon";
|
||||
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";
|
||||
|
||||
const LOCALIZE_KEYS: Record<string, string> = {
|
||||
"ui.common.copy": "Copy",
|
||||
"ui.common.show": "Show",
|
||||
"ui.common.hide": "Hide",
|
||||
"ui.common.add": "Add",
|
||||
"ui.common.remove": "Remove",
|
||||
"ui.common.search": "Search",
|
||||
"ui.common.copied_clipboard": "Copied to clipboard",
|
||||
};
|
||||
|
||||
@customElement("demo-components-ha-input")
|
||||
export class DemoHaInput extends LitElement {
|
||||
constructor() {
|
||||
super();
|
||||
// Provides localizeContext 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,
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-input in ${mode}">
|
||||
<div class="card-content">
|
||||
<h3>Basic</h3>
|
||||
<div class="row">
|
||||
<ha-input label="Default"></ha-input>
|
||||
<ha-input label="With value" value="Hello"></ha-input>
|
||||
<ha-input
|
||||
label="With placeholder"
|
||||
placeholder="Type here..."
|
||||
></ha-input>
|
||||
</div>
|
||||
|
||||
<h3>Input types</h3>
|
||||
<div class="row">
|
||||
<ha-input label="Text" type="text" value="Text"></ha-input>
|
||||
<ha-input label="Number" type="number" value="42"></ha-input>
|
||||
<ha-input
|
||||
label="Email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Password"
|
||||
type="password"
|
||||
value="secret"
|
||||
password-toggle
|
||||
></ha-input>
|
||||
<ha-input label="URL" type="url" placeholder="https://...">
|
||||
</ha-input>
|
||||
<ha-input label="Date" type="date"></ha-input>
|
||||
</div>
|
||||
|
||||
<h3>States</h3>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Disabled"
|
||||
disabled
|
||||
value="Disabled"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
label="Readonly"
|
||||
readonly
|
||||
value="Readonly"
|
||||
></ha-input>
|
||||
<ha-input label="Required" required></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
label="Invalid"
|
||||
invalid
|
||||
validation-message="This field is required"
|
||||
value=""
|
||||
></ha-input>
|
||||
<ha-input label="With hint" hint="This is a hint"></ha-input>
|
||||
<ha-input
|
||||
label="With clear"
|
||||
with-clear
|
||||
value="Clear me"
|
||||
></ha-input>
|
||||
</div>
|
||||
|
||||
<h3>With slots</h3>
|
||||
<div class="row">
|
||||
<ha-input label="With prefix">
|
||||
<span slot="start">$</span>
|
||||
</ha-input>
|
||||
<ha-input label="With suffix">
|
||||
<span slot="end">kg</span>
|
||||
</ha-input>
|
||||
<ha-input label="With icon">
|
||||
<ha-svg-icon .path=${mdiMagnify} slot="start"></ha-svg-icon>
|
||||
</ha-input>
|
||||
</div>
|
||||
|
||||
<h3>Appearance: outlined</h3>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined"
|
||||
value="Hello"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined disabled"
|
||||
disabled
|
||||
value="Disabled"
|
||||
></ha-input>
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
label="Outlined invalid"
|
||||
invalid
|
||||
validation-message="Required"
|
||||
></ha-input>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ha-input
|
||||
appearance="outlined"
|
||||
placeholder="Placeholder only"
|
||||
></ha-input>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<ha-card header="Derivatives in ${mode}">
|
||||
<div class="card-content">
|
||||
<h3>ha-input-search</h3>
|
||||
<ha-input-search label="Search label"></ha-input-search>
|
||||
<ha-input-search appearance="outlined"></ha-input-search>
|
||||
|
||||
<h3>ha-input-copy</h3>
|
||||
<ha-input-copy
|
||||
value="my-api-token-123"
|
||||
masked-value="••••••••••••••••••"
|
||||
masked-toggle
|
||||
></ha-input-copy>
|
||||
|
||||
<h3>ha-input-multi</h3>
|
||||
<ha-input-multi
|
||||
label="URL"
|
||||
add-label="Add URL"
|
||||
.value=${["https://example.com"]}
|
||||
></ha-input-multi>
|
||||
</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-input": DemoHaInput;
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -129,7 +129,6 @@
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "3.0.0",
|
||||
"ua-parser-js": "2.0.9",
|
||||
"weekstart": "2.0.0",
|
||||
"workbox-cacheable-response": "7.4.0",
|
||||
"workbox-core": "7.4.0",
|
||||
@@ -169,7 +168,6 @@
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.1.0",
|
||||
"babel-loader": "10.1.1",
|
||||
@@ -230,6 +228,6 @@
|
||||
},
|
||||
"packageManager": "yarn@4.13.0",
|
||||
"volta": {
|
||||
"node": "24.14.0"
|
||||
"node": "24.14.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260128.0"
|
||||
version = "20260325.5"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -58,9 +58,11 @@ export class CastManager {
|
||||
this._eventListeners[event].push(listener);
|
||||
|
||||
return () => {
|
||||
this._eventListeners[event].splice(
|
||||
this._eventListeners[event].indexOf(listener)
|
||||
);
|
||||
const listeners = this._eventListeners[event];
|
||||
const index = listeners.indexOf(listener);
|
||||
if (index !== -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,14 @@ import type {
|
||||
EntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { stripPrefixFromEntityName } from "./strip_prefix_from_entity_name";
|
||||
|
||||
export const computeEntityName = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"]
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): string | undefined => {
|
||||
const entry = entities[stateObj.entity_id] as
|
||||
| EntityRegistryDisplayEntry
|
||||
@@ -18,22 +21,49 @@ export const computeEntityName = (
|
||||
// Fall back to state name if not in the entity registry (friendly name)
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
return computeEntityEntryName(entry);
|
||||
return computeEntityEntryName(entry, devices);
|
||||
};
|
||||
|
||||
export const computeEntityEntryName = (
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry
|
||||
entry: EntityRegistryDisplayEntry | EntityRegistryEntry,
|
||||
devices: HomeAssistant["devices"],
|
||||
fallbackStateObj?: HassEntity
|
||||
): string | undefined => {
|
||||
if (entry.name != null) {
|
||||
return entry.name;
|
||||
const name =
|
||||
entry.name ||
|
||||
("original_name" in entry && entry.original_name != null
|
||||
? String(entry.original_name)
|
||||
: undefined);
|
||||
|
||||
const device = entry.device_id ? devices[entry.device_id] : undefined;
|
||||
|
||||
if (!device) {
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
if (fallbackStateObj) {
|
||||
return computeStateName(fallbackStateObj);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if ("original_name" in entry && entry.original_name != null) {
|
||||
return String(entry.original_name);
|
||||
|
||||
const deviceName = computeDeviceName(device);
|
||||
|
||||
// If the device name is the same as the entity name, consider empty entity name
|
||||
if (deviceName === name) {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
// Remove the device name from the entity name if it starts with it
|
||||
if (deviceName && name) {
|
||||
return stripPrefixFromEntityName(name, deviceName) || name;
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
|
||||
export const entityUseDeviceName = (
|
||||
stateObj: HassEntity,
|
||||
entities: HomeAssistant["entities"]
|
||||
): boolean => !computeEntityName(stateObj, entities);
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"]
|
||||
): boolean => !computeEntityName(stateObj, entities, devices);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { computeAreaName } from "./compute_area_name";
|
||||
import { computeDeviceName } from "./compute_device_name";
|
||||
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
|
||||
import { computeFloorName } from "./compute_floor_name";
|
||||
import { computeStateName } from "./compute_state_name";
|
||||
import { getEntityContext } from "./context/get_entity_context";
|
||||
|
||||
const DEFAULT_SEPARATOR = " ";
|
||||
@@ -29,14 +30,23 @@ export interface EntityNameOptions {
|
||||
|
||||
export const computeEntityNameDisplay = (
|
||||
stateObj: HassEntity,
|
||||
name: EntityNameItem | EntityNameItem[] | undefined,
|
||||
name: string | EntityNameItem | EntityNameItem[] | undefined,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
floors: HomeAssistant["floors"],
|
||||
options?: EntityNameOptions
|
||||
) => {
|
||||
let items = ensureArray(name || DEFAULT_ENTITY_NAME);
|
||||
if (typeof name === "string") {
|
||||
return name;
|
||||
}
|
||||
|
||||
// If no name config is provided, fall back to the friendly name
|
||||
if (!name) {
|
||||
return computeStateName(stateObj);
|
||||
}
|
||||
|
||||
let items = ensureArray(name);
|
||||
|
||||
const separator = options?.separator ?? DEFAULT_SEPARATOR;
|
||||
|
||||
@@ -45,7 +55,7 @@ export const computeEntityNameDisplay = (
|
||||
return items.map((item) => item.text).join(separator);
|
||||
}
|
||||
|
||||
const useDeviceName = entityUseDeviceName(stateObj, entities);
|
||||
const useDeviceName = entityUseDeviceName(stateObj, entities, devices);
|
||||
|
||||
// If entity uses device name, and device is not already included, replace it with device name
|
||||
if (useDeviceName) {
|
||||
@@ -91,7 +101,7 @@ export const computeEntityNameList = (
|
||||
const names = name.map((item) => {
|
||||
switch (item.type) {
|
||||
case "entity":
|
||||
return computeEntityName(stateObj, entities);
|
||||
return computeEntityName(stateObj, entities, devices);
|
||||
case "device":
|
||||
return device ? computeDeviceName(device) : undefined;
|
||||
case "area":
|
||||
|
||||
@@ -142,9 +142,10 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
group: "value",
|
||||
decimal: "value",
|
||||
fraction: "value",
|
||||
minusSign: "value",
|
||||
plusSign: "value",
|
||||
literal: "literal",
|
||||
currency: "unit",
|
||||
minusSign: "value",
|
||||
};
|
||||
|
||||
const valueParts: ValuePart[] = [];
|
||||
@@ -153,7 +154,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
const type = TYPE_MAP[part.type];
|
||||
if (!type) continue;
|
||||
const last = valueParts[valueParts.length - 1];
|
||||
// Merge consecutive numeric parts (e.g. "1" + "," + "234" + "." + "56" → "1,234.56")
|
||||
// Merge consecutive value parts (e.g. "-" + "12" + "." + "00" → "-12.00")
|
||||
if (type === "value" && last?.type === "value") {
|
||||
last.value += part.value;
|
||||
} else {
|
||||
@@ -254,6 +255,7 @@ const computeStateToPartsFromEntityAttributes = (
|
||||
"conversation",
|
||||
"event",
|
||||
"image",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"notify",
|
||||
"scene",
|
||||
|
||||
@@ -29,6 +29,7 @@ export const FIXED_DOMAIN_STATES = {
|
||||
device_tracker: ["home", "not_home"],
|
||||
fan: ["on", "off"],
|
||||
humidifier: ["on", "off"],
|
||||
infrared: [],
|
||||
input_boolean: ["on", "off"],
|
||||
input_button: [],
|
||||
lawn_mower: ["error", "paused", "mowing", "returning", "docked"],
|
||||
@@ -270,6 +271,8 @@ export const getStates = (
|
||||
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":
|
||||
|
||||
@@ -6,7 +6,9 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const compareState = state !== undefined ? state : stateObj?.state;
|
||||
|
||||
if (["button", "event", "input_button", "scene"].includes(domain)) {
|
||||
if (
|
||||
["button", "event", "infrared", "input_button", "scene"].includes(domain)
|
||||
) {
|
||||
return compareState !== UNAVAILABLE;
|
||||
}
|
||||
|
||||
|
||||
11
src/common/feature-detect/support-popover.ts
Normal file
11
src/common/feature-detect/support-popover.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Indicates whether the current browser supports the Popover API.
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Popover_API
|
||||
*/
|
||||
export const popoverSupported = globalThis?.HTMLElement?.prototype
|
||||
? Object.prototype.hasOwnProperty.call(
|
||||
globalThis.HTMLElement.prototype,
|
||||
"popover"
|
||||
)
|
||||
: false;
|
||||
@@ -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,
|
||||
|
||||
@@ -280,18 +280,23 @@ export class HaChartBase extends LitElement {
|
||||
<div class="chart"></div>
|
||||
</div>
|
||||
${this._renderLegend()}
|
||||
<div class="chart-controls ${classMap({ small: this.smallControls })}">
|
||||
${this._isZoomed && !this.hideResetButton
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
.path=${mdiRestart}
|
||||
@click=${this._handleZoomReset}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.history_charts.zoom_reset"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<slot name="button"></slot>
|
||||
<div class="top-controls ${classMap({ small: this.smallControls })}">
|
||||
<slot name="search"></slot>
|
||||
<div
|
||||
class="chart-controls ${classMap({ small: this.smallControls })}"
|
||||
>
|
||||
${this._isZoomed && !this.hideResetButton
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
.path=${mdiRestart}
|
||||
@click=${this._handleZoomReset}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.history_charts.zoom_reset"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
<slot name="button"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -585,7 +590,10 @@ 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.
|
||||
filterMode: "boundaryFilter" as any,
|
||||
// Only use it for line charts — it causes issues with bar charts.
|
||||
filterMode: (ensureArray(this.data).every((s) => s.type === "line")
|
||||
? "boundaryFilter"
|
||||
: "filter") as any,
|
||||
xAxisIndex: 0,
|
||||
moveOnMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
preventDefaultMouseMove: !this._isTouchDevice || this._isZoomed,
|
||||
@@ -627,7 +635,7 @@ export class HaChartBase extends LitElement {
|
||||
hideOverlap: true,
|
||||
...axis.axisLabel,
|
||||
},
|
||||
minInterval,
|
||||
minInterval: axis.minInterval ?? minInterval,
|
||||
} as XAXisOption;
|
||||
});
|
||||
}
|
||||
@@ -1116,16 +1124,35 @@ export class HaChartBase extends LitElement {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.chart-controls {
|
||||
.top-controls {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 4px;
|
||||
top: var(--ha-space-4);
|
||||
inset-inline-start: var(--ha-space-4);
|
||||
inset-inline-end: var(--ha-space-1);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--ha-space-2);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
::slotted([slot="search"]) {
|
||||
flex: 1 1 250px;
|
||||
min-width: 0;
|
||||
max-width: 250px;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.chart-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-1);
|
||||
margin-inline-start: auto;
|
||||
flex-shrink: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.top-controls.small {
|
||||
top: 0;
|
||||
}
|
||||
.chart-controls.small {
|
||||
top: 0;
|
||||
flex-direction: row;
|
||||
}
|
||||
.chart-controls ha-icon-button,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { GraphSeriesOption } from "echarts/charts";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
|
||||
import type {
|
||||
CallbackDataParams,
|
||||
TopLevelFormatterParams,
|
||||
@@ -63,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");
|
||||
|
||||
@@ -76,11 +80,23 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
params: TopLevelFormatterParams
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* Optional callback that returns additional searchable strings for a node.
|
||||
* These are matched against the search filter in addition to the node's name and context.
|
||||
*/
|
||||
@property({ attribute: false }) public searchableAttributes?: (
|
||||
nodeId: string
|
||||
) => string[];
|
||||
|
||||
@property({ attribute: false }) public searchFilter = "";
|
||||
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@state() private _highlightedNodes?: Set<string>;
|
||||
|
||||
@state() private _reducedMotion = false;
|
||||
|
||||
@state() private _physicsEnabled = true;
|
||||
@state() private _physicsEnabled?: boolean;
|
||||
|
||||
@state() private _showLabels = true;
|
||||
|
||||
@@ -108,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;
|
||||
@@ -117,19 +141,24 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
|
||||
const hasHighlightedNodes =
|
||||
this._highlightedNodes && this._highlightedNodes.size > 0;
|
||||
|
||||
return html`<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
.data=${this._getSeries(
|
||||
this.data,
|
||||
this._physicsEnabled,
|
||||
this._physicsEnabled ?? false,
|
||||
this._reducedMotion,
|
||||
this._showLabels,
|
||||
isMobile
|
||||
isMobile,
|
||||
hasHighlightedNodes
|
||||
)}
|
||||
.options=${this._createOptions(this.data?.categories)}
|
||||
height="100%"
|
||||
.extraComponents=${[GraphChart]}
|
||||
>
|
||||
<slot name="search" slot="search"></slot>
|
||||
<slot name="button" slot="button"></slot>
|
||||
<ha-icon-button
|
||||
slot="button"
|
||||
@@ -165,7 +194,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
...category,
|
||||
icon: category.symbol,
|
||||
})),
|
||||
top: 8,
|
||||
bottom: 8,
|
||||
},
|
||||
dataZoom: {
|
||||
type: "inside",
|
||||
@@ -175,13 +204,56 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
deepEqual
|
||||
);
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (changedProperties.has("searchFilter")) {
|
||||
const filter = this.searchFilter;
|
||||
if (!filter) {
|
||||
this._highlightedNodes = undefined;
|
||||
} else {
|
||||
const lowerFilter = filter.toLowerCase();
|
||||
const matchingIds = new Set<string>();
|
||||
for (const node of this.data.nodes) {
|
||||
if (this._nodeMatchesFilter(node, lowerFilter)) {
|
||||
matchingIds.add(node.id);
|
||||
}
|
||||
}
|
||||
this._highlightedNodes = matchingIds;
|
||||
}
|
||||
this._applyHighlighting();
|
||||
this._updateMouseoverHandler();
|
||||
}
|
||||
}
|
||||
|
||||
private _nodeMatchesFilter(node: NetworkNode, lowerFilter: string): boolean {
|
||||
if (node.name?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
if (node.context?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
if (node.id?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
if (this.searchableAttributes) {
|
||||
const extraValues = this.searchableAttributes(node.id);
|
||||
for (const value of extraValues) {
|
||||
if (value?.toLowerCase().includes(lowerFilter)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _getSeries = memoizeOne(
|
||||
(
|
||||
data: NetworkData,
|
||||
physicsEnabled: boolean,
|
||||
reducedMotion: boolean,
|
||||
showLabels: boolean,
|
||||
isMobile: boolean
|
||||
isMobile: boolean,
|
||||
hasHighlightedNodes?: boolean
|
||||
) => ({
|
||||
id: "network",
|
||||
type: "graph",
|
||||
@@ -214,7 +286,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
},
|
||||
emphasis: {
|
||||
focus: isMobile ? "none" : "adjacency",
|
||||
focus: hasHighlightedNodes ? "self" : isMobile ? "none" : "adjacency",
|
||||
},
|
||||
force: {
|
||||
repulsion: [400, 600],
|
||||
@@ -362,6 +434,68 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _applyHighlighting() {
|
||||
const chart = this._baseChart?.chart;
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
// Reset all nodes to normal opacity first
|
||||
chart.dispatchAction({ type: "downplay" });
|
||||
|
||||
const highlighted = this._highlightedNodes;
|
||||
if (!highlighted || highlighted.size === 0) {
|
||||
return;
|
||||
}
|
||||
const dataIndices: number[] = [];
|
||||
this.data.nodes.forEach((node, index) => {
|
||||
if (highlighted.has(node.id)) {
|
||||
dataIndices.push(index);
|
||||
}
|
||||
});
|
||||
if (dataIndices.length > 0) {
|
||||
chart.dispatchAction({ type: "highlight", dataIndex: dataIndices });
|
||||
}
|
||||
}
|
||||
|
||||
private _emphasisGuardHandler?: () => void;
|
||||
|
||||
private _updateMouseoverHandler() {
|
||||
const chart = this._baseChart?.chart;
|
||||
if (!chart) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When there are highlighted nodes, re-apply highlighting on hover
|
||||
// and mouseout to prevent hover from overriding the search state
|
||||
if (this._highlightedNodes && this._highlightedNodes.size > 0) {
|
||||
if (this._emphasisGuardHandler) {
|
||||
// Guard already set
|
||||
return;
|
||||
}
|
||||
this._emphasisGuardHandler = () => {
|
||||
this._applyHighlighting();
|
||||
};
|
||||
chart.on("mouseover", this._emphasisGuardHandler);
|
||||
chart.on("mouseout", this._emphasisGuardHandler);
|
||||
} else {
|
||||
if (!this._emphasisGuardHandler) {
|
||||
return;
|
||||
}
|
||||
chart.off("mouseover", this._emphasisGuardHandler);
|
||||
chart.off("mouseout", this._emphasisGuardHandler);
|
||||
this._emphasisGuardHandler = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._emphasisGuardHandler) {
|
||||
this._baseChart?.chart?.off("mouseover", this._emphasisGuardHandler);
|
||||
this._baseChart?.chart?.off("mouseout", this._emphasisGuardHandler);
|
||||
this._emphasisGuardHandler = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _togglePhysics() {
|
||||
this._saveNodePositions();
|
||||
this._physicsEnabled = !this._physicsEnabled;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -27,7 +27,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-svg-icon";
|
||||
import "../search-input";
|
||||
import "../input/ha-input-search";
|
||||
import { filterData, sortData } from "./sort-filter";
|
||||
|
||||
export interface RowClickedEvent {
|
||||
@@ -391,11 +391,11 @@ export class HaDataTable extends LitElement {
|
||||
${this._filterable
|
||||
? html`
|
||||
<div class="table-header">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.searchLabel}
|
||||
></search-input>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
@input=${this._handleSearchChange}
|
||||
.placeholder=${this.searchLabel}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
@@ -970,12 +970,12 @@ export class HaDataTable extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent): void {
|
||||
private _handleSearchChange(ev: InputEvent): void {
|
||||
if (this.filter) {
|
||||
return;
|
||||
}
|
||||
this._lastSelectedRowId = null;
|
||||
this._debounceSearch(ev.detail.value);
|
||||
this._debounceSearch((ev.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
private async _calcTableHeight() {
|
||||
@@ -1388,11 +1388,9 @@ export class HaDataTable extends LitElement {
|
||||
.table-header {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
ha-input-search {
|
||||
flex: 1;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: transparent;
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
slot[name="header"] {
|
||||
display: block;
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiCalendarToday } from "@mdi/js";
|
||||
import "cally";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
|
||||
import {
|
||||
formatCallyDateRange,
|
||||
@@ -19,7 +19,12 @@ import {
|
||||
localizeContext,
|
||||
} from "../../data/context";
|
||||
import { TimeZone } from "../../data/translation";
|
||||
import { MobileAwareMixin } from "../../mixins/mobile-aware-mixin";
|
||||
import { haStyleScrollbar } from "../../resources/styles";
|
||||
import type { 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,11 +32,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";
|
||||
|
||||
@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;
|
||||
@@ -69,6 +75,8 @@ export class DateRangePicker extends LitElement {
|
||||
to: { hours: 23, minutes: 59 },
|
||||
};
|
||||
|
||||
@queryAll("ha-time-input") private _timeInputs?: NodeListOf<HaTimeInput>;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -100,16 +108,38 @@ 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
|
||||
@@ -153,6 +183,7 @@ export class DateRangePicker extends LitElement {
|
||||
)}
|
||||
id="from"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
<ha-time-input
|
||||
.value=${`${this._timeValue.to.hours}:${this._timeValue.to.minutes}`}
|
||||
@@ -163,6 +194,7 @@ export class DateRangePicker extends LitElement {
|
||||
)}
|
||||
id="to"
|
||||
placeholder-labels
|
||||
auto-validate
|
||||
></ha-time-input>
|
||||
</div>
|
||||
`
|
||||
@@ -200,6 +232,14 @@ export class DateRangePicker extends LitElement {
|
||||
let endDate = new Date(`${dates[1]}T23:59:00`);
|
||||
|
||||
if (this.timePicker) {
|
||||
const timeInputs = this._timeInputs;
|
||||
if (
|
||||
timeInputs &&
|
||||
![...timeInputs].every((input) => input.reportValidity())
|
||||
) {
|
||||
// If we have time inputs, and they don't all report valid, don't save
|
||||
return;
|
||||
}
|
||||
startDate.setHours(this._timeValue.from.hours);
|
||||
startDate.setMinutes(this._timeValue.from.minutes);
|
||||
endDate.setHours(this._timeValue.to.hours);
|
||||
@@ -257,31 +297,38 @@ 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._dateValue = formatCallyDateRange(
|
||||
dateRange[0],
|
||||
dateRange[1],
|
||||
this.locale,
|
||||
this.hassConfig
|
||||
);
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleChangeTime(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
const time = ev.detail.value;
|
||||
const type = (ev.target as HaBaseTimeInput).id;
|
||||
const target = ev.target as HaBaseTimeInput;
|
||||
const type = target.id;
|
||||
if (time) {
|
||||
if (!this._timeValue) {
|
||||
this._timeValue = {
|
||||
@@ -298,20 +345,48 @@ export class DateRangePicker extends LitElement {
|
||||
static styles = [
|
||||
datePickerStyles,
|
||||
dateRangePickerStyles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
.picker {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
.range {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
padding: var(--ha-space-3);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.picker {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.date-range-ranges {
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
margin-top: var(--ha-space-5);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-3);
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.range {
|
||||
flex-basis: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.times {
|
||||
@@ -326,12 +401,6 @@ export class DateRangePicker extends LitElement {
|
||||
padding: var(--ha-space-2);
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.date-range-ranges {
|
||||
max-width: 30%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -80,33 +80,6 @@ export const datePickerStyles = css`
|
||||
text-align: center;
|
||||
margin-left: 48px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
calendar-month {
|
||||
min-height: calc(34px * 7);
|
||||
}
|
||||
calendar-month::part(day) {
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
calendar-month::part(button) {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
}
|
||||
calendar-month::part(range-inner),
|
||||
calendar-month::part(range-start),
|
||||
calendar-month::part(range-end),
|
||||
calendar-month::part(selected),
|
||||
calendar-month::part(selected):hover {
|
||||
height: 34px;
|
||||
width: 34px;
|
||||
}
|
||||
.heading {
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
.month-year {
|
||||
margin-left: 40px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const dateRangePickerStyles = css`
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import {
|
||||
DEFAULT_ENTITY_NAME,
|
||||
type EntityNameItem,
|
||||
} from "../../common/entity/compute_entity_name_display";
|
||||
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import type { EntityNameType } from "../../common/translations/entity-state";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
@@ -17,12 +15,14 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import "../ha-sortable";
|
||||
import "../input/ha-input";
|
||||
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
@@ -73,10 +73,291 @@ export class HaEntityNamePicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||
@state() private _mode?: "composed" | "custom";
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated) {
|
||||
const items = this._toItems(this.value);
|
||||
this._mode =
|
||||
items.length === 1 && items[0].type === "text" ? "custom" : "composed";
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(_changedProperties: PropertyValues): void {
|
||||
if (this._mode === undefined) {
|
||||
const items = this._toItems(this.value);
|
||||
this._mode =
|
||||
items.length === 1 && items[0].type === "text" ? "custom" : "composed";
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const modeButtons = [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.mode_composed"
|
||||
),
|
||||
value: "composed",
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.mode_custom"
|
||||
),
|
||||
value: "custom",
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${modeButtons}
|
||||
.active=${this._mode}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._modeChanged}
|
||||
></ha-button-toggle-group>
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._mode === "custom"
|
||||
? this._renderTextInput()
|
||||
: this._renderPicker()}
|
||||
</div>
|
||||
</div>
|
||||
${this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTextInput() {
|
||||
const items = this._items;
|
||||
const value =
|
||||
items.length === 1 && items[0].type === "text" ? items[0].text || "" : "";
|
||||
return html`
|
||||
<ha-input
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.value=${value}
|
||||
@input=${this._textInputChanged}
|
||||
></ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPicker() {
|
||||
const value = this._items;
|
||||
const validTypes = this._validTypes(this.entityId);
|
||||
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.value=${this._getPickerValue()}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="field" class="field">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
(item: EntityNameItem, idx) => {
|
||||
const label = this._formatItem(item);
|
||||
const isValid = validTypes.has(item.type);
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _modeChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._mode = ev.detail.value as "composed" | "custom";
|
||||
}
|
||||
|
||||
private _textInputChanged(ev: Event) {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
const newValue: EntityNameItem[] = value
|
||||
? [{ type: "text", text: value }]
|
||||
: [];
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
const value = this._items[idx];
|
||||
// Pre-fill the field value when editing a text item
|
||||
if (value.type === "text" && value.text) {
|
||||
this._picker?.setFieldValue(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._items;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const value = [...this._items];
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item: EntityNameItem = parseOptionValue(value);
|
||||
|
||||
const newValue = [...this._items];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = item;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(item);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: EntityNameItem[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private get _items(): EntityNameItem[] {
|
||||
return this._toItems(this.value);
|
||||
}
|
||||
|
||||
private _toItems = memoizeOne((value?: typeof this.value) => {
|
||||
if (typeof value === "string") {
|
||||
if (value === "") {
|
||||
return [];
|
||||
}
|
||||
return [{ type: "text", text: value } satisfies EntityNameItem];
|
||||
}
|
||||
return value ? ensureArray(value) : [];
|
||||
});
|
||||
|
||||
private _toValue = memoizeOne(
|
||||
(items: EntityNameItem[]): typeof this.value => {
|
||||
if (items.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
return item.type === "text" ? item.text : item;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _formatItem = (item: EntityNameItem) => {
|
||||
if (item.type === "text") {
|
||||
return `"${item.text}"`;
|
||||
}
|
||||
if (KNOWN_TYPES.has(item.type)) {
|
||||
return this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
|
||||
);
|
||||
}
|
||||
return item.type;
|
||||
};
|
||||
|
||||
private _validTypes = memoizeOne((entityId?: string) => {
|
||||
const options = new Set<string>(["text"]);
|
||||
if (!entityId) {
|
||||
@@ -161,157 +442,6 @@ export class HaEntityNamePicker extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _formatItem = (item: EntityNameItem) => {
|
||||
if (item.type === "text") {
|
||||
return `"${item.text}"`;
|
||||
}
|
||||
if (KNOWN_TYPES.has(item.type)) {
|
||||
return this.hass.localize(
|
||||
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
|
||||
);
|
||||
}
|
||||
return item.type;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
const value = this._items;
|
||||
const validTypes = this._validTypes(this.entityId);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.value=${this._getPickerValue()}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
this._items,
|
||||
(item) => item,
|
||||
(item: EntityNameItem, idx) => {
|
||||
const label = this._formatItem(item);
|
||||
const isValid = validTypes.has(item.type);
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.add"
|
||||
)}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx || "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
const value = this._items[idx];
|
||||
// Pre-fill the field value when editing a text item
|
||||
if (value.type === "text" && value.text) {
|
||||
this._picker?.setFieldValue(value.text);
|
||||
}
|
||||
}
|
||||
|
||||
private get _items(): EntityNameItem[] {
|
||||
return this._toItems(this.value);
|
||||
}
|
||||
|
||||
private _toItems = memoizeOne((value?: typeof this.value) => {
|
||||
if (typeof value === "string") {
|
||||
if (value === "") {
|
||||
return [];
|
||||
}
|
||||
return [{ type: "text", text: value } satisfies EntityNameItem];
|
||||
}
|
||||
return value ? ensureArray(value) : [...DEFAULT_ENTITY_NAME];
|
||||
});
|
||||
|
||||
private _toValue = memoizeOne(
|
||||
(items: EntityNameItem[]): typeof this.value => {
|
||||
if (items.length === 0) {
|
||||
return "";
|
||||
}
|
||||
if (items.length === 1) {
|
||||
const item = items[0];
|
||||
return item.type === "text" ? item.text : item;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getPickerValue(): string | undefined {
|
||||
if (this._editIndex != null) {
|
||||
const item = this._items[this._editIndex];
|
||||
@@ -362,58 +492,6 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
const value = this._items;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
const value = [...this._items];
|
||||
const idx = parseInt((ev.target as HTMLElement).dataset.idx || "", 10);
|
||||
value.splice(idx, 1);
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item: EntityNameItem = parseOptionValue(value);
|
||||
|
||||
const newValue = [...this._items];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = item;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(item);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: EntityNameItem[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
@@ -421,13 +499,42 @@ export class HaEntityNamePicker extends LitElement {
|
||||
}
|
||||
|
||||
.container {
|
||||
--ha-input-padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
ha-generic-picker,
|
||||
ha-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-end-end-radius: var(--ha-border-radius-square);
|
||||
border-end-start-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
.container:after {
|
||||
.field:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -445,30 +552,25 @@ export class HaEntityNamePicker extends LitElement {
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
:host([disabled]) .container:after {
|
||||
:host([disabled]) .field:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
.container:focus-within:after {
|
||||
.field:focus-within:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.add {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-2) var(--ha-space-2);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-relative-time";
|
||||
import "./state-badge";
|
||||
import "../ha-tooltip";
|
||||
import "./state-badge";
|
||||
|
||||
@customElement("state-info")
|
||||
class StateInfo extends LitElement {
|
||||
@@ -22,7 +21,7 @@ class StateInfo extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const name = computeStateName(this.stateObj);
|
||||
const name = this.hass.formatEntityName(this.stateObj, { type: "entity" });
|
||||
|
||||
return html`<state-badge
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -21,7 +21,6 @@ class AliasesEditor extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-input-multi
|
||||
.hass=${this.hass}
|
||||
.value=${this.aliases}
|
||||
.disabled=${this.disabled}
|
||||
.sortable=${this.sortable}
|
||||
|
||||
@@ -9,7 +9,6 @@ import "./ha-expansion-panel";
|
||||
import "./ha-items-display-editor";
|
||||
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
|
||||
export interface AreasDisplayValue {
|
||||
hidden?: string[];
|
||||
|
||||
@@ -15,7 +15,6 @@ import "./ha-floor-icon";
|
||||
import "./ha-items-display-editor";
|
||||
import type { DisplayItem, DisplayValue } from "./ha-items-display-editor";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
|
||||
export interface AreasFloorsDisplayValue {
|
||||
areas_display?: {
|
||||
|
||||
@@ -56,7 +56,10 @@ class HaAttributeValue extends LitElement {
|
||||
this.stateObj!,
|
||||
this.attribute
|
||||
);
|
||||
return parts.find((part) => part.type === "value")?.value;
|
||||
return parts
|
||||
.filter((part) => part.type === "value")
|
||||
.map((part) => part.value)
|
||||
.join("");
|
||||
}
|
||||
|
||||
return this.hass.formatEntityAttributeValue(this.stateObj!, this.attribute);
|
||||
|
||||
@@ -137,7 +137,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
@property({ attribute: "placeholder-labels", type: Boolean })
|
||||
public placeholderLabels = false;
|
||||
|
||||
@queryAll("ha-input") private _inputs?: HaInput[];
|
||||
@queryAll("ha-input") private _inputs?: NodeListOf<HaInput>;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
@@ -145,7 +145,9 @@ export class HaBaseTimeInput extends LitElement {
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._inputs?.every((input) => input.reportValidity()) ?? true;
|
||||
const inputs = this._inputs;
|
||||
if (!inputs) return true;
|
||||
return [...inputs].every((input) => input.reportValidity());
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -399,7 +401,7 @@ export class HaBaseTimeInput extends LitElement {
|
||||
|
||||
.time-separator,
|
||||
ha-icon-button {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
background-color: var(--ha-color-form-background);
|
||||
color: var(--ha-color-text-secondary);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-loud);
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -57,6 +57,7 @@ export class HaButton extends Button {
|
||||
.button {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: 1;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
transition: background-color var(--ha-animation-duration-fast)
|
||||
ease-out;
|
||||
|
||||
@@ -1,522 +0,0 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import { resolveTimeZone } from "../common/datetime/resolve-time-zone";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import {
|
||||
CLOCK_CARD_DATE_PARTS,
|
||||
formatClockCardDate,
|
||||
} from "../panels/lovelace/cards/clock/clock-date-format";
|
||||
import type { ClockCardDatePart } from "../panels/lovelace/cards/types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import "./chips/ha-assist-chip";
|
||||
import "./chips/ha-chip-set";
|
||||
import "./chips/ha-input-chip";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import "./ha-input-helper-text";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
import "./ha-sortable";
|
||||
|
||||
type ClockDatePartSection = "weekday" | "day" | "month" | "year" | "separator";
|
||||
|
||||
type ClockDateSeparatorPart = Extract<
|
||||
ClockCardDatePart,
|
||||
"separator-dash" | "separator-slash" | "separator-dot" | "separator-new-line"
|
||||
>;
|
||||
|
||||
const CLOCK_DATE_PART_SECTION_ORDER: readonly ClockDatePartSection[] = [
|
||||
"day",
|
||||
"month",
|
||||
"year",
|
||||
"weekday",
|
||||
"separator",
|
||||
];
|
||||
|
||||
const CLOCK_DATE_SEPARATOR_VALUES: Record<ClockDateSeparatorPart, string> = {
|
||||
"separator-dash": "-",
|
||||
"separator-slash": "/",
|
||||
"separator-dot": ".",
|
||||
"separator-new-line": "",
|
||||
};
|
||||
|
||||
const getClockDatePartSection = (
|
||||
part: ClockCardDatePart
|
||||
): ClockDatePartSection => {
|
||||
if (part.startsWith("weekday-")) {
|
||||
return "weekday";
|
||||
}
|
||||
|
||||
if (part.startsWith("day-")) {
|
||||
return "day";
|
||||
}
|
||||
|
||||
if (part.startsWith("month-")) {
|
||||
return "month";
|
||||
}
|
||||
|
||||
if (part.startsWith("year-")) {
|
||||
return "year";
|
||||
}
|
||||
|
||||
return "separator";
|
||||
};
|
||||
|
||||
interface ClockDatePartSectionData {
|
||||
id: ClockDatePartSection;
|
||||
title: string;
|
||||
items: PickerComboBoxItem[];
|
||||
}
|
||||
|
||||
interface ClockDatePartValueItem {
|
||||
key: string;
|
||||
item: string;
|
||||
idx: number;
|
||||
}
|
||||
|
||||
@customElement("ha-clock-date-format-picker")
|
||||
export class HaClockDateFormatPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string[] | string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@query("ha-generic-picker", true) private _picker?: HaGenericPicker;
|
||||
|
||||
private _editIndex?: number;
|
||||
|
||||
protected render() {
|
||||
const value = this._value;
|
||||
const valueItems = this._getValueItems(value);
|
||||
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.value=${this._getPickerValue()}
|
||||
.sections=${this._getSections(this.hass.locale.language)}
|
||||
.getItems=${this._getItems}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
<ha-sortable
|
||||
no-style
|
||||
@item-moved=${this._moveItem}
|
||||
.disabled=${this.disabled}
|
||||
handle-selector="button.primary.action"
|
||||
filter=".add"
|
||||
>
|
||||
<ha-chip-set>
|
||||
${repeat(
|
||||
valueItems,
|
||||
(entry: ClockDatePartValueItem) => entry.key,
|
||||
({ item, idx }) => this._renderValueChip(item, idx)
|
||||
)}
|
||||
${this.disabled
|
||||
? nothing
|
||||
: html`
|
||||
<ha-assist-chip
|
||||
@click=${this._addItem}
|
||||
.disabled=${this.disabled}
|
||||
label=${this.hass.localize("ui.common.add")}
|
||||
class="add"
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
`}
|
||||
</ha-chip-set>
|
||||
</ha-sortable>
|
||||
</div>
|
||||
</ha-generic-picker>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderHelper() {
|
||||
return this.helper
|
||||
? html`
|
||||
<ha-input-helper-text .disabled=${this.disabled}>
|
||||
${this.helper}
|
||||
</ha-input-helper-text>
|
||||
`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _getValueItems = memoizeOne(
|
||||
(value: string[]): ClockDatePartValueItem[] => {
|
||||
const occurrences = new Map<string, number>();
|
||||
|
||||
return value.map((item, idx) => {
|
||||
const occurrence = occurrences.get(item) ?? 0;
|
||||
occurrences.set(item, occurrence + 1);
|
||||
|
||||
return {
|
||||
key: `${item}:${occurrence}`,
|
||||
item,
|
||||
idx,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
private _renderValueChip(item: string, idx: number) {
|
||||
const label = this._getItemLabel(item, this.hass.locale.language);
|
||||
const isValid = !!label;
|
||||
|
||||
return html`
|
||||
<ha-input-chip
|
||||
data-idx=${idx}
|
||||
@remove=${this._removeItem}
|
||||
@click=${this._editItem}
|
||||
.label=${label ?? item}
|
||||
.selected=${!this.disabled}
|
||||
.disabled=${this.disabled}
|
||||
class=${!isValid ? "invalid" : ""}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiDragHorizontalVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-input-chip>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _addItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._editIndex = undefined;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private async _editItem(ev: Event) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx ?? "",
|
||||
10
|
||||
);
|
||||
this._editIndex = idx;
|
||||
await this.updateComplete;
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return !this.value ? [] : ensureArray(this.value);
|
||||
}
|
||||
|
||||
private _toValue = memoizeOne((value: string[]): string[] | undefined =>
|
||||
value.length === 0 ? undefined : value
|
||||
);
|
||||
|
||||
private _buildSections = memoizeOne(
|
||||
(language: string): ClockDatePartSectionData[] => {
|
||||
const itemsBySection: Record<ClockDatePartSection, PickerComboBoxItem[]> =
|
||||
{
|
||||
weekday: [],
|
||||
day: [],
|
||||
month: [],
|
||||
year: [],
|
||||
separator: [],
|
||||
};
|
||||
|
||||
const previewDate = new Date();
|
||||
const previewTimeZone = resolveTimeZone(
|
||||
this.hass.locale.time_zone,
|
||||
this.hass.config.time_zone
|
||||
);
|
||||
|
||||
CLOCK_CARD_DATE_PARTS.forEach((part) => {
|
||||
const section = getClockDatePartSection(part);
|
||||
const label =
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.parts.${part}`
|
||||
) ?? part;
|
||||
|
||||
const secondary =
|
||||
section === "separator"
|
||||
? CLOCK_DATE_SEPARATOR_VALUES[part as ClockDateSeparatorPart]
|
||||
: formatClockCardDate(
|
||||
previewDate,
|
||||
{ parts: [part] },
|
||||
language,
|
||||
previewTimeZone
|
||||
);
|
||||
|
||||
itemsBySection[section].push({
|
||||
id: part,
|
||||
primary: label,
|
||||
secondary,
|
||||
sorting_label: label,
|
||||
});
|
||||
});
|
||||
|
||||
return CLOCK_DATE_PART_SECTION_ORDER.map((section) => ({
|
||||
id: section,
|
||||
title:
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.date.sections.${section}`
|
||||
) ?? section,
|
||||
items: itemsBySection[section],
|
||||
})).filter((section) => section.items.length > 0);
|
||||
}
|
||||
);
|
||||
|
||||
private _getSections = memoizeOne(
|
||||
(_language: string): { id: string; label: string }[] =>
|
||||
this._buildSections(_language).map((section) => ({
|
||||
id: section.id,
|
||||
label: section.title,
|
||||
}))
|
||||
);
|
||||
|
||||
private _getItems = (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
): (PickerComboBoxItem | string)[] => {
|
||||
const normalizedSearch = searchString?.trim().toLowerCase();
|
||||
|
||||
const sections = this._buildSections(this.hass.locale.language)
|
||||
.map((sectionData) => {
|
||||
if (!normalizedSearch) {
|
||||
return sectionData;
|
||||
}
|
||||
|
||||
return {
|
||||
...sectionData,
|
||||
items: sectionData.items.filter(
|
||||
(item) =>
|
||||
item.primary.toLowerCase().includes(normalizedSearch) ||
|
||||
item.secondary?.toLowerCase().includes(normalizedSearch) ||
|
||||
item.id.toLowerCase().includes(normalizedSearch)
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter((sectionData) => sectionData.items.length > 0);
|
||||
|
||||
if (section) {
|
||||
return (
|
||||
sections.find((candidate) => candidate.id === section)?.items || []
|
||||
);
|
||||
}
|
||||
|
||||
const groupedItems: (PickerComboBoxItem | string)[] = [];
|
||||
|
||||
sections.forEach((sectionData) => {
|
||||
groupedItems.push(sectionData.title, ...sectionData.items);
|
||||
});
|
||||
|
||||
return groupedItems;
|
||||
};
|
||||
|
||||
private _getItemLabel = memoizeOne(
|
||||
(value: string, language: string): string | undefined => {
|
||||
const sections = this._buildSections(language);
|
||||
|
||||
for (const section of sections) {
|
||||
const item = section.items.find((candidate) => candidate.id === value);
|
||||
|
||||
if (item) {
|
||||
if (section.id === "separator") {
|
||||
if (value === "separator-new-line") {
|
||||
return item.primary;
|
||||
}
|
||||
|
||||
return item.secondary ?? item.primary;
|
||||
}
|
||||
|
||||
return `${item.secondary} [${item.primary} ${section.title}]`;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
);
|
||||
|
||||
private _getPickerValue(): string | undefined {
|
||||
if (this._editIndex != null) {
|
||||
return this._value[this._editIndex];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
const value = this._value;
|
||||
const newValue = value.concat();
|
||||
const element = newValue.splice(oldIndex, 1)[0];
|
||||
newValue.splice(newIndex, 0, element);
|
||||
|
||||
this._setValue(newValue);
|
||||
}
|
||||
|
||||
private async _removeItem(ev: Event) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
const idx = parseInt(
|
||||
(ev.currentTarget as HTMLElement).dataset.idx ?? "",
|
||||
10
|
||||
);
|
||||
|
||||
if (Number.isNaN(idx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = [...this._value];
|
||||
value.splice(idx, 1);
|
||||
|
||||
if (this._editIndex !== undefined) {
|
||||
if (this._editIndex === idx) {
|
||||
this._editIndex = undefined;
|
||||
} else if (this._editIndex > idx) {
|
||||
this._editIndex -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
this._setValue(value);
|
||||
}
|
||||
|
||||
private _pickerValueChanged(ev: ValueChangedEvent<string>): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
|
||||
if (this.disabled || !value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = [...this._value];
|
||||
|
||||
if (this._editIndex != null) {
|
||||
newValue[this._editIndex] = value;
|
||||
this._editIndex = undefined;
|
||||
} else {
|
||||
newValue.push(value);
|
||||
}
|
||||
|
||||
this._setValue(newValue);
|
||||
|
||||
if (this._picker) {
|
||||
this._picker.value = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _setValue(value: string[]) {
|
||||
const newValue = this._toValue(value);
|
||||
this.value = newValue;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
border-end-end-radius: var(--ha-border-radius-square);
|
||||
border-end-start-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
||||
.container:after {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
transition:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
}
|
||||
|
||||
:host([disabled]) .container:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
|
||||
.container:focus-within:after {
|
||||
height: 2px;
|
||||
background-color: var(--mdc-theme-primary);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
}
|
||||
|
||||
.add {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
ha-chip-set {
|
||||
padding: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.invalid {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
ha-input-helper-text {
|
||||
display: block;
|
||||
margin: var(--ha-space-2) 0 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-clock-date-format-picker": HaClockDateFormatPicker;
|
||||
}
|
||||
}
|
||||
@@ -100,6 +100,9 @@ export class HaDropdown extends Dropdown {
|
||||
#menu {
|
||||
padding: var(--ha-space-1);
|
||||
}
|
||||
wa-popup::part(popup) {
|
||||
z-index: 200;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./search-input-outlined";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-devices")
|
||||
export class HaFilterDevices extends LitElement {
|
||||
@@ -67,12 +68,12 @@ export class HaFilterDevices extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
</ha-input-search>
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._devices(
|
||||
@@ -138,8 +139,9 @@ export class HaFilterDevices extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
private _devices = memoizeOne(
|
||||
@@ -249,7 +251,7 @@ export class HaFilterDevices extends LitElement {
|
||||
ha-check-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./search-input-outlined";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-domains")
|
||||
export class HaFilterDomains extends LitElement {
|
||||
@@ -49,12 +50,12 @@ export class HaFilterDomains extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@click=${this._handleItemClick}
|
||||
@@ -155,8 +156,9 @@ export class HaFilterDomains extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -201,7 +203,7 @@ export class HaFilterDomains extends LitElement {
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ import "./ha-check-list-item";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./ha-state-icon";
|
||||
import "./search-input-outlined";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-entities")
|
||||
export class HaFilterEntities extends LitElement {
|
||||
@@ -70,12 +71,12 @@ export class HaFilterEntities extends LitElement {
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`
|
||||
<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
</ha-input-search>
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._entities(
|
||||
@@ -149,8 +150,9 @@ export class HaFilterEntities extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
private _entities = memoizeOne(
|
||||
@@ -265,7 +267,7 @@ export class HaFilterEntities extends LitElement {
|
||||
--mdc-list-item-graphic-margin: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ import "./ha-check-list-item";
|
||||
import "./ha-domain-icon";
|
||||
import "./ha-expansion-panel";
|
||||
import "./ha-list";
|
||||
import "./search-input-outlined";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-integrations")
|
||||
export class HaFilterIntegrations extends LitElement {
|
||||
@@ -52,12 +53,12 @@ export class HaFilterIntegrations extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
${this._manifests && this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
class="ha-scrollbar"
|
||||
@click=${this._handleItemClick}
|
||||
@@ -175,8 +176,9 @@ export class HaFilterIntegrations extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
this._filter = (target.value ?? "").toLowerCase();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -221,7 +223,7 @@ export class HaFilterIntegrations extends LitElement {
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import { computeCssColor } from "../common/color/compute-color";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { navigate } from "../common/navigate";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { labelsContext } from "../data/context";
|
||||
import type { LabelRegistryEntry } from "../data/label/label_registry";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-check-list-item";
|
||||
@@ -21,7 +21,8 @@ import "./ha-icon-button";
|
||||
import "./ha-label";
|
||||
import "./ha-list";
|
||||
import "./ha-list-item";
|
||||
import "./search-input-outlined";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
@customElement("ha-filter-labels")
|
||||
export class HaFilterLabels extends LitElement {
|
||||
@@ -79,12 +80,12 @@ export class HaFilterLabels extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
${this._shouldRender
|
||||
? html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
? html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input-outlined>
|
||||
</ha-input-search>
|
||||
<ha-list
|
||||
@selected=${this._labelSelected}
|
||||
class="ha-scrollbar"
|
||||
@@ -163,8 +164,9 @@ export class HaFilterLabels extends LitElement {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value.toLowerCase();
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const value = (ev.target as HaInputSearch).value ?? "";
|
||||
this._filter = value.toLowerCase();
|
||||
}
|
||||
|
||||
private async _labelSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
|
||||
@@ -261,7 +263,7 @@ export class HaFilterLabels extends LitElement {
|
||||
right: 0;
|
||||
left: 0;
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
|
||||
@@ -142,6 +142,19 @@ export const computeInitialHaFormData = (
|
||||
])[firstChoice],
|
||||
};
|
||||
}
|
||||
} else if ("numeric_threshold" in selector) {
|
||||
const mode = selector.numeric_threshold?.mode ?? "crossed";
|
||||
const type = mode === "changed" ? "any" : "above";
|
||||
data[field.name] =
|
||||
type === "any"
|
||||
? { type }
|
||||
: {
|
||||
type,
|
||||
value: {
|
||||
number: selector.numeric_threshold?.number?.min ?? 0,
|
||||
active_choice: "number",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
throw new Error(
|
||||
`Selector ${Object.keys(selector)[0]} not supported in initial form data`
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import "../input/ha-input";
|
||||
import type { HaInput } from "../input/ha-input";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormFloatData,
|
||||
@@ -25,7 +25,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
@query("ha-input", true) private _input?: HaInput;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
@@ -38,23 +38,25 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="any"
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.hint=${this.helper}
|
||||
.value=${this.data !== undefined ? this.data : ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
.suffix=${this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required
|
||||
? this.localize?.("ui.common.error_required")
|
||||
: undefined}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
@input=${this._handleInput}
|
||||
>
|
||||
${this.schema.description?.suffix
|
||||
? html`<span slot="end">${this.schema.description?.suffix}</span>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -64,9 +66,9 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
const source = ev.target as HaTextField;
|
||||
const rawValue = source.value.replace(",", ".");
|
||||
private _handleInput(ev: InputEvent) {
|
||||
const source = ev.target as HaInput;
|
||||
const rawValue = (source.value ?? "").replace(",", ".");
|
||||
|
||||
let value: number | undefined;
|
||||
|
||||
@@ -74,6 +76,11 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow user to keep typing decimal places (e.g., 5.0, 5.00, 5.10)
|
||||
if (rawValue.includes(".") && rawValue.endsWith("0")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow user to start typing a negative value
|
||||
if (rawValue === "-") {
|
||||
return;
|
||||
@@ -105,9 +112,6 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
:host([own-margin]) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
@@ -7,8 +7,8 @@ import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import "../input/ha-input";
|
||||
import type { HaInput } from "../input/ha-input";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormIntegerData,
|
||||
@@ -29,8 +29,8 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield, ha-slider", true) private _input?:
|
||||
| HaTextField
|
||||
@query("ha-input, ha-slider", true) private _input?:
|
||||
| HaInput
|
||||
| HTMLInputElement;
|
||||
|
||||
private _lastValue?: HaFormIntegerData;
|
||||
@@ -89,28 +89,30 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
>${this.helper}</ha-input-helper-text
|
||||
>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
helperPersistent
|
||||
.value=${this.data !== undefined ? this.data : ""}
|
||||
.hint=${this.helper}
|
||||
.value=${this.data !== undefined ? this.data.toString() : ""}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.schema.required}
|
||||
.autoValidate=${this.schema.required}
|
||||
.suffix=${this.schema.description?.suffix}
|
||||
.validationMessage=${this.schema.required
|
||||
? this.localize?.("ui.common.error_required")
|
||||
: undefined}
|
||||
@input=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
>
|
||||
${this.schema.description?.suffix
|
||||
? html`<span slot="end">${this.schema.description.suffix}</span>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -167,8 +169,8 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _valueChanged(ev: Event) {
|
||||
const source = ev.target as HaTextField | HTMLInputElement;
|
||||
private _valueChanged(ev: InputEvent) {
|
||||
const source = ev.target as HaInput | HTMLInputElement;
|
||||
const rawValue = source.value;
|
||||
|
||||
let value: number | undefined;
|
||||
@@ -201,9 +203,6 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
ha-slider {
|
||||
flex: 1;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -199,11 +199,6 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
padding-inline-start: initial;
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-icon-button {
|
||||
color: var(--input-dropdown-icon-color);
|
||||
position: absolute;
|
||||
|
||||
@@ -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 -36,-2 L -44,-1 A 1,1,0,0,0,-44,1 L -36,2 A 2,2,0,0,0,-36,-2 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: 6;
|
||||
stroke-linecap: butt;
|
||||
}
|
||||
|
||||
.level {
|
||||
fill: none;
|
||||
stroke-width: 8;
|
||||
stroke-width: 6;
|
||||
stroke-linecap: butt;
|
||||
}
|
||||
|
||||
.value {
|
||||
fill: none;
|
||||
stroke-width: 8;
|
||||
stroke-width: 6;
|
||||
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);
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { OutlinedTextField } from "@material/web/textfield/internal/outlined-text-field";
|
||||
import { styles } from "@material/web/textfield/internal/outlined-styles";
|
||||
import { styles as sharedStyles } from "@material/web/textfield/internal/shared-styles";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { literal } from "lit/static-html";
|
||||
import "./ha-outlined-field";
|
||||
|
||||
@customElement("ha-outlined-text-field")
|
||||
export class HaOutlinedTextField extends OutlinedTextField {
|
||||
protected readonly fieldTag = literal`ha-outlined-field`;
|
||||
|
||||
static override styles = [
|
||||
sharedStyles,
|
||||
styles,
|
||||
css`
|
||||
:host {
|
||||
--md-sys-color-on-surface: var(--primary-text-color);
|
||||
--md-sys-color-primary: var(--primary-text-color);
|
||||
--md-outlined-text-field-input-text-color: var(--primary-text-color);
|
||||
--md-sys-color-on-surface-variant: var(--secondary-text-color);
|
||||
--md-outlined-field-outline-color: var(--outline-color);
|
||||
--md-outlined-field-focus-outline-color: var(--primary-color);
|
||||
--md-outlined-field-hover-outline-color: var(--outline-hover-color);
|
||||
}
|
||||
:host([dense]) {
|
||||
--md-outlined-field-top-space: 5.5px;
|
||||
--md-outlined-field-bottom-space: 5.5px;
|
||||
--md-outlined-field-container-shape-start-start: 10px;
|
||||
--md-outlined-field-container-shape-start-end: 10px;
|
||||
--md-outlined-field-container-shape-end-end: 10px;
|
||||
--md-outlined-field-container-shape-end-start: 10px;
|
||||
--md-outlined-field-focus-outline-width: 1px;
|
||||
--md-outlined-field-with-leading-content-leading-space: 8px;
|
||||
--md-outlined-field-with-trailing-content-trailing-space: 8px;
|
||||
--md-outlined-field-content-space: 8px;
|
||||
--mdc-icon-size: var(--md-input-chip-icon-size, 18px);
|
||||
}
|
||||
.input {
|
||||
font-family: var(--ha-font-family-body);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-outlined-text-field": HaOutlinedTextField;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import { mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -30,8 +30,8 @@ import "./ha-combo-box-item";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
import "./input/ha-input-search";
|
||||
import type { HaInputSearch } from "./input/ha-input-search";
|
||||
|
||||
export const DEFAULT_SEARCH_KEYS: FuseWeightedKey[] = [
|
||||
{
|
||||
@@ -159,7 +159,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@query("lit-virtualizer") public virtualizerElement?: LitVirtualizer;
|
||||
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
@query("ha-input-search") private _searchFieldElement?: HaInputSearch;
|
||||
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
@@ -226,19 +226,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
"Search | Add custom value")
|
||||
: (this.localize?.("ui.common.search") ?? "Search"));
|
||||
|
||||
return html`<ha-textfield
|
||||
.label=${searchLabel}
|
||||
return html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.placeholder=${searchLabel}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@input=${this._filterChanged}
|
||||
.iconTrailing=${this.clearable && !!this._search}
|
||||
>
|
||||
<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
slot="trailingIcon"
|
||||
.label=${this.localize?.("ui.common.clear") || "Clear"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</ha-textfield>
|
||||
</ha-input-search>
|
||||
${this._renderSectionButtons()}
|
||||
${this.sections?.length
|
||||
? html`
|
||||
@@ -455,21 +449,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
fireEvent(this, "index-selected", { index, newTab });
|
||||
}
|
||||
|
||||
private _clearSearch = () => {
|
||||
if (this._searchFieldElement) {
|
||||
this._searchFieldElement.value = "";
|
||||
this._searchFieldElement.dispatchEvent(new Event("input"));
|
||||
}
|
||||
};
|
||||
|
||||
private _fuseIndex = memoizeOne(
|
||||
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) =>
|
||||
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
|
||||
);
|
||||
|
||||
private _filterChanged = (ev: Event) => {
|
||||
const textfield = ev.target as HaTextField;
|
||||
const searchString = textfield.value.trim();
|
||||
private _filterChanged = (ev: InputEvent) => {
|
||||
const textfield = ev.target as HaInputSearch;
|
||||
const searchString = (textfield.value ?? "").trim();
|
||||
this._search = searchString;
|
||||
|
||||
if (this.sections?.length) {
|
||||
@@ -810,12 +797,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
--text-field-padding-end: 0;
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
ha-input-search {
|
||||
padding: 0 var(--ha-space-3);
|
||||
margin-bottom: var(--ha-space-3);
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) ha-textfield {
|
||||
:host([mode="dialog"]) ha-input-search {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
@@ -929,7 +915,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
padding: var(--ha-space-1) var(--ha-space-4);
|
||||
}
|
||||
|
||||
:host([mode="dialog"]) ha-textfield {
|
||||
:host([mode="dialog"]) ha-input-search {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,8 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
css`
|
||||
ha-combo-box-item[disabled] {
|
||||
background-color: var(--ha-color-form-background-disabled);
|
||||
--md-list-item-disabled-opacity: 0.5;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
ha-combo-box-item {
|
||||
@@ -141,13 +143,6 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
--md-focus-ring-duration: 0s;
|
||||
}
|
||||
|
||||
/* Add Similar focus style as the text field */
|
||||
ha-combo-box-item[disabled]:after {
|
||||
background-color: var(
|
||||
--mdc-text-field-disabled-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
}
|
||||
ha-combo-box-item:after {
|
||||
display: block;
|
||||
content: "";
|
||||
@@ -158,10 +153,7 @@ export class HaPickerField extends PickerMixin(LitElement) {
|
||||
right: 0;
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
background-color: var(
|
||||
--mdc-text-field-idle-line-color,
|
||||
rgba(0, 0, 0, 0.42)
|
||||
);
|
||||
background-color: var(--ha-color-border-neutral-loud);
|
||||
transform:
|
||||
height 180ms ease-in-out,
|
||||
background-color 180ms ease-in-out;
|
||||
|
||||
@@ -17,8 +17,8 @@ import "./ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-spinner";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
import "./input/ha-input";
|
||||
import type { HaInput } from "./input/ha-input";
|
||||
|
||||
prepareZXingModule({
|
||||
overrides: {
|
||||
@@ -64,7 +64,7 @@ class HaQrScanner extends LitElement {
|
||||
|
||||
@query("#canvas-container", true) private _canvasContainer?: HTMLDivElement;
|
||||
|
||||
@query("ha-textfield") private _manualInput?: HaTextField;
|
||||
@query("ha-input") private _manualInput?: HaInput;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
@@ -153,13 +153,13 @@ class HaQrScanner extends LitElement {
|
||||
</ha-alert>
|
||||
<p>${this.hass.localize("ui.components.qr-scanner.manual_input")}</p>
|
||||
<div class="row">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.qr-scanner.enter_qr_code"
|
||||
)}
|
||||
@keyup=${this._manualKeyup}
|
||||
@paste=${this._manualPaste}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
<ha-button @click=${this._manualSubmit}>
|
||||
${this.hass.localize("ui.common.submit")}
|
||||
</ha-button>
|
||||
@@ -242,7 +242,7 @@ class HaQrScanner extends LitElement {
|
||||
|
||||
private _manualKeyup(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter") {
|
||||
this._qrCodeScanned((ev.target as HaTextField).value);
|
||||
this._qrCodeScanned((ev.target as HaInput).value ?? "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ class HaQrScanner extends LitElement {
|
||||
}
|
||||
|
||||
private _manualSubmit() {
|
||||
this._qrCodeScanned(this._manualInput!.value);
|
||||
this._qrCodeScanned(this._manualInput!.value ?? "");
|
||||
}
|
||||
|
||||
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
|
||||
@@ -382,7 +382,7 @@ class HaQrScanner extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
|
||||
@@ -3,13 +3,10 @@ import { customElement, property } from "lit/decorators";
|
||||
import { hex2rgb, rgb2hex } from "../../common/color/convert-color";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ColorRGBSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-textfield";
|
||||
import "../input/ha-input";
|
||||
|
||||
@customElement("ha-selector-color_rgb")
|
||||
export class HaColorRGBSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ColorRGBSelector;
|
||||
|
||||
@property() public value?: string;
|
||||
@@ -24,16 +21,15 @@ export class HaColorRGBSelector extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
type="color"
|
||||
helperPersistent
|
||||
.value=${this.value ? rgb2hex(this.value as any) : ""}
|
||||
.label=${this.label || ""}
|
||||
.required=${this.required}
|
||||
.helper=${this.helper}
|
||||
.hint=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._valueChanged}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -50,14 +46,10 @@ export class HaColorRGBSelector extends LitElement {
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
ha-textfield {
|
||||
--text-field-padding-top: 8px;
|
||||
--text-field-padding-bottom: 8px;
|
||||
--text-field-padding-start: 8px;
|
||||
--text-field-padding-end: 8px;
|
||||
ha-input {
|
||||
min-width: 75px;
|
||||
flex-grow: 1;
|
||||
margin: 0 4px;
|
||||
margin: 0 var(--ha-space-1);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NumberSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import "../input/ha-input";
|
||||
import type { HaInput } from "../input/ha-input";
|
||||
|
||||
@customElement("ha-selector-number")
|
||||
export class HaNumberSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: NumberSelector;
|
||||
|
||||
@property({ type: Number }) public value?: number;
|
||||
@@ -31,7 +27,7 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield", true) private _input?: HaTextField | HTMLInputElement;
|
||||
@query("ha-input", true) private _input?: HaInput;
|
||||
|
||||
private _valueStr = "";
|
||||
|
||||
@@ -99,29 +95,30 @@ export class HaNumberSelector extends LitElement {
|
||||
</ha-slider>
|
||||
`
|
||||
: nothing}
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.inputMode=${this.selector.number?.step === "any" ||
|
||||
(this.selector.number?.step ?? 1) % 1 !== 0
|
||||
? "decimal"
|
||||
: "numeric"}
|
||||
.label=${!isBox ? undefined : this.label}
|
||||
.placeholder=${this.placeholder}
|
||||
class=${classMap({ single: isBox })}
|
||||
.placeholder=${this.placeholder !== undefined
|
||||
? this.placeholder.toString()
|
||||
: ""}
|
||||
class=${isBox ? "single" : ""}
|
||||
.min=${this.selector.number?.min}
|
||||
.max=${this.selector.number?.max}
|
||||
.value=${this._valueStr ?? ""}
|
||||
.step=${this.selector.number?.step ?? 1}
|
||||
helperPersistent
|
||||
.helper=${isBox ? this.helper : undefined}
|
||||
.hint=${isBox ? this.helper : undefined}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.suffix=${unit}
|
||||
type="number"
|
||||
autoValidate
|
||||
?no-spinner=${!isBox}
|
||||
.withoutSpinButtons=${!isBox}
|
||||
@input=${this._handleInputChange}
|
||||
>
|
||||
</ha-textfield>
|
||||
${unit ? html`<span slot="end">${unit}</span>` : nothing}
|
||||
</ha-input>
|
||||
</div>
|
||||
${!isBox && this.helper
|
||||
? html`<ha-input-helper-text .disabled=${this.disabled}
|
||||
@@ -166,11 +163,10 @@ export class HaNumberSelector extends LitElement {
|
||||
margin-inline-end: 16px;
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
ha-textfield {
|
||||
--ha-textfield-input-width: 40px;
|
||||
ha-input::part(wa-input) {
|
||||
width: 40px;
|
||||
}
|
||||
.single {
|
||||
--ha-textfield-input-width: unset;
|
||||
ha-input.single {
|
||||
flex: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import {
|
||||
mdiArrowCollapseVertical,
|
||||
mdiArrowExpandVertical,
|
||||
mdiGreaterThan,
|
||||
mdiLessThan,
|
||||
} from "@mdi/js";
|
||||
import { mdiChartBellCurveCumulative } from "@mdi/js";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NumericThresholdSelector } from "../../data/selector";
|
||||
import type {
|
||||
NumericThresholdSelector,
|
||||
ThresholdMode,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button-toggle-group";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select";
|
||||
import "./ha-selector";
|
||||
|
||||
type ThresholdType = "above" | "below" | "between" | "outside";
|
||||
const iconThresholdAbove =
|
||||
"M 2 2 L 2 22 L 22 22 L 22 20 L 4 20 L 4 2 L 2 2 z M 17.976562 6 C 17.965863 6.00017 17.951347 6.0014331 17.935547 6.0019531 C 17.903847 6.0030031 17.862047 6.0043225 17.810547 6.0078125 C 17.707247 6.0148425 17.564772 6.0273144 17.388672 6.0527344 C 17.036572 6.1035743 16.54829 6.2035746 15.962891 6.3964844 C 14.788292 6.783584 13.232027 7.5444846 11.611328 9.0332031 C 10.753918 9.820771 9.8854345 10.808987 9.0449219 12.042969 C 7.881634 12.257047 7 13.274809 7 14.5 C 7 15.880699 8.1192914 17 9.5 17 C 10.880699 17 12 15.880699 12 14.5 C 12 13.732663 11.653544 13.046487 11.109375 12.587891 C 11.732682 11.74814 12.359503 11.061942 12.964844 10.505859 C 14.359842 9.2245207 15.662945 8.6023047 16.589844 8.296875 C 17.054643 8.1437252 17.426428 8.0689231 17.673828 8.0332031 C 17.797428 8.0153531 17.891466 8.0076962 17.947266 8.0039062 C 17.974966 8.0020263 17.992753 8.0003 18.001953 8 L 17.998047 6 L 17.976562 6 z";
|
||||
|
||||
const iconThresholdBelow =
|
||||
"M 2 2 L 2 22 L 22 22 L 22 20 L 4 20 L 4 2 L 2 2 z M 9.5 7 C 8.1192914 7 7 8.1192914 7 9.5 C 7 10.880699 8.1192914 12 9.5 12 C 9.598408 12 9.6955741 11.993483 9.7910156 11.982422 C 10.39444 12.754246 11.005767 13.410563 11.611328 13.966797 C 13.232027 15.455495 14.788292 16.216416 15.962891 16.603516 C 16.54829 16.796415 17.036572 16.896366 17.388672 16.947266 C 17.564772 16.972666 17.707247 16.985188 17.810547 16.992188 C 17.862047 16.995687 17.903847 16.997047 17.935547 16.998047 C 17.951347 16.998547 17.965863 16.9998 17.976562 17 L 17.998047 17 L 18.001953 15 C 17.992753 14.9997 17.974966 14.999947 17.947266 14.998047 C 17.891466 14.994247 17.797428 14.984597 17.673828 14.966797 C 17.426428 14.931097 17.054643 14.856325 16.589844 14.703125 C 15.662945 14.397725 14.359842 13.775439 12.964844 12.494141 C 12.496227 12.063656 12.015935 11.551532 11.533203 10.955078 C 11.826929 10.545261 12 10.042666 12 9.5 C 12 8.1192914 10.880699 7 9.5 7 z";
|
||||
|
||||
const iconThresholdBetween =
|
||||
"M 2 2 L 2 22 L 22 22 L 22 20 L 4 20 L 4 2 L 2 2 z M 16.5 4 C 15.119301 4 14 5.1192914 14 6.5 C 14 6.8572837 14.075904 7.196497 14.210938 7.5039062 C 13.503071 8.3427071 12.800578 9.3300361 12.130859 10.501953 C 11.718781 11.223082 11.287475 11.849823 10.845703 12.394531 C 10.457136 12.145771 9.9956073 12 9.5 12 C 8.1192914 12 7 13.119301 7 14.5 C 7 15.880699 8.1192914 17 9.5 17 C 10.880699 17 12 15.880699 12 14.5 C 12 14.38201 11.990422 14.26598 11.974609 14.152344 C 12.636605 13.409426 13.276156 12.531884 13.869141 11.494141 C 14.462491 10.455789 15.073208 9.5905169 15.681641 8.8613281 C 15.938115 8.9501682 16.213303 9 16.5 9 C 17.880699 9 19 7.8807086 19 6.5 C 19 5.1192914 17.880699 4 16.5 4 z";
|
||||
|
||||
const iconThresholdOutside =
|
||||
"M 2 2 L 2 22 L 22 22 L 22 20 L 4 20 L 4 19.046875 C 4.226574 19.041905 4.4812768 19.028419 4.7597656 19 C 5.8832145 18.8854 7.4011147 18.537974 9.0019531 17.609375 L 8.8847656 17.408203 C 9.320466 17.777433 9.8841605 18 10.5 18 C 11.880699 18 13 16.880699 13 15.5 C 13 14.119301 11.880699 13 10.5 13 C 9.1192914 13 8 14.119301 8 15.5 C 8 15.654727 8.0141099 15.806171 8.0410156 15.953125 L 7.9980469 15.876953 C 6.6882482 16.636752 5.4555097 16.918066 4.5566406 17.009766 C 4.3512557 17.030705 4.166436 17.040275 4 17.044922 L 4 2 L 2 2 z M 21.976562 4 C 21.965863 4.00017 21.951347 4.0014331 21.935547 4.0019531 C 21.903847 4.0030031 21.862047 4.0043225 21.810547 4.0078125 C 21.707247 4.0148425 21.564772 4.0273144 21.388672 4.0527344 C 21.036572 4.1035743 20.54829 4.2035846 19.962891 4.3964844 C 19.34193 4.6011277 18.613343 4.9149715 17.826172 5.3808594 C 17.441793 5.1398775 16.987134 5 16.5 5 C 15.119301 5 14 6.1192914 14 7.5 C 14 8.8807086 15.119301 10 16.5 10 C 17.880699 10 19 8.8807086 19 7.5 C 19 7.3403872 18.983669 7.1845035 18.955078 7.0332031 C 19.569666 6.6795942 20.126994 6.4493921 20.589844 6.296875 C 21.054643 6.1437252 21.426428 6.0689231 21.673828 6.0332031 C 21.797428 6.0153531 21.891466 6.0076962 21.947266 6.0039062 C 21.974966 6.0020263 21.992753 6.0003 22.001953 6 L 21.998047 4 L 21.976562 4 z";
|
||||
|
||||
type ThresholdType = "above" | "below" | "between" | "outside" | "any";
|
||||
|
||||
interface ThresholdValueEntry {
|
||||
active_choice?: string;
|
||||
@@ -31,6 +42,12 @@ interface NumericThresholdValue {
|
||||
value_max?: ThresholdValueEntry;
|
||||
}
|
||||
|
||||
const DEFAULT_TYPE: Record<ThresholdMode, ThresholdType> = {
|
||||
crossed: "above",
|
||||
changed: "any",
|
||||
is: "above",
|
||||
};
|
||||
|
||||
@customElement("ha-selector-numeric_threshold")
|
||||
export class HaNumericThresholdSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -49,9 +66,26 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
|
||||
@state() private _type?: ThresholdType;
|
||||
|
||||
private _getMode(): ThresholdMode {
|
||||
return this.selector.numeric_threshold?.mode ?? "crossed";
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("value")) {
|
||||
this._type = this.value?.type || "above";
|
||||
if (changedProperties.has("value") || changedProperties.has("selector")) {
|
||||
const mode = this._getMode();
|
||||
this._type = this.value?.type || DEFAULT_TYPE[mode];
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
(changedProperties.has("value") || changedProperties.has("selector")) &&
|
||||
!this.value
|
||||
) {
|
||||
const mode = this._getMode();
|
||||
const type = DEFAULT_TYPE[mode];
|
||||
fireEvent(this, "value-changed", { value: { type } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,41 +117,13 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const type = this._type || "above";
|
||||
const mode = this._getMode();
|
||||
const type = this._type || DEFAULT_TYPE[mode];
|
||||
const showSingleValue = type === "above" || type === "below";
|
||||
const showRangeValues = type === "between" || type === "outside";
|
||||
const unitOptions = this._getUnitOptions();
|
||||
|
||||
const typeOptions = [
|
||||
{
|
||||
value: "above",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.above"
|
||||
),
|
||||
iconPath: mdiGreaterThan,
|
||||
},
|
||||
{
|
||||
value: "below",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.below"
|
||||
),
|
||||
iconPath: mdiLessThan,
|
||||
},
|
||||
{
|
||||
value: "between",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.in_range"
|
||||
),
|
||||
iconPath: mdiArrowCollapseVertical,
|
||||
},
|
||||
{
|
||||
value: "outside",
|
||||
label: this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.outside_range"
|
||||
),
|
||||
iconPath: mdiArrowExpandVertical,
|
||||
},
|
||||
];
|
||||
const typeOptions = this._buildTypeOptions(this.hass.localize, mode);
|
||||
|
||||
const choiceToggleButtons = [
|
||||
{
|
||||
@@ -134,6 +140,16 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
},
|
||||
];
|
||||
|
||||
// Value-row label for single-value types (above/below).
|
||||
const singleValueLabel = this.hass.localize(
|
||||
`ui.components.selectors.numeric_threshold.${mode}.${type as "above" | "below"}`
|
||||
);
|
||||
|
||||
// Determine which type-select label to use per mode
|
||||
const typeSelectLabel = this.hass.localize(
|
||||
`ui.components.selectors.numeric_threshold.${mode}.type`
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
${this.label
|
||||
@@ -141,9 +157,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
: nothing}
|
||||
<div class="inputs">
|
||||
<ha-select
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.selectors.numeric_threshold.type"
|
||||
)}
|
||||
.label=${typeSelectLabel}
|
||||
.value=${type}
|
||||
.options=${typeOptions}
|
||||
.disabled=${this.disabled}
|
||||
@@ -152,11 +166,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
|
||||
${showSingleValue
|
||||
? this._renderValueRow(
|
||||
this.hass.localize(
|
||||
type === "above"
|
||||
? "ui.components.selectors.numeric_threshold.above"
|
||||
: "ui.components.selectors.numeric_threshold.below"
|
||||
),
|
||||
singleValueLabel,
|
||||
this.value?.value,
|
||||
this._valueChanged,
|
||||
this._valueChoiceChanged,
|
||||
@@ -199,6 +209,40 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _buildTypeOptions = memoizeOne(
|
||||
(localize: HomeAssistant["localize"], mode: ThresholdMode) => {
|
||||
const baseOptions = (
|
||||
[
|
||||
{ value: "above", iconPath: iconThresholdAbove },
|
||||
{ value: "below", iconPath: iconThresholdBelow },
|
||||
{ value: "between", iconPath: iconThresholdBetween },
|
||||
{ value: "outside", iconPath: iconThresholdOutside },
|
||||
] as const
|
||||
).map(({ value, iconPath }) => ({
|
||||
value,
|
||||
iconPath,
|
||||
label: localize(
|
||||
`ui.components.selectors.numeric_threshold.${mode}.${value}`
|
||||
),
|
||||
}));
|
||||
|
||||
if (mode !== "changed") {
|
||||
return baseOptions;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
value: "any",
|
||||
iconPath: mdiChartBellCurveCumulative,
|
||||
label: localize(
|
||||
"ui.components.selectors.numeric_threshold.changed.any"
|
||||
),
|
||||
},
|
||||
...baseOptions,
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
private _renderUnitSelect(
|
||||
entry: ThresholdValueEntry | undefined,
|
||||
handler: (ev: CustomEvent) => void,
|
||||
@@ -243,7 +287,9 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
const numberSelector = {
|
||||
number: {
|
||||
...this.selector.numeric_threshold?.number,
|
||||
...(effectiveUnit ? { unit_of_measurement: effectiveUnit } : {}),
|
||||
...(!showUnit && effectiveUnit
|
||||
? { unit_of_measurement: effectiveUnit }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
const entitySelector = {
|
||||
@@ -255,9 +301,11 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
return html`
|
||||
<div class="value-row">
|
||||
<div class="value-header">
|
||||
<span class="value-label"
|
||||
>${rowLabel}${this.required ? "*" : ""}</span
|
||||
>
|
||||
${rowLabel
|
||||
? html`<span class="value-label"
|
||||
>${rowLabel}${this.required ? "*" : ""}</span
|
||||
>`
|
||||
: nothing}
|
||||
<ha-button-toggle-group
|
||||
size="small"
|
||||
.buttons=${choiceToggleButtons}
|
||||
@@ -302,6 +350,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
newValue.value_min = this.value?.value_min ?? this.value?.value;
|
||||
newValue.value_max = this.value?.value_max;
|
||||
}
|
||||
// "any" type has no value fields
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
}
|
||||
@@ -428,6 +477,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
|
||||
.inputs,
|
||||
.value-row {
|
||||
--ha-input-padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-2);
|
||||
@@ -447,7 +497,7 @@ export class HaNumericThresholdSelector extends LitElement {
|
||||
.value-inputs {
|
||||
display: flex;
|
||||
gap: var(--ha-space-2);
|
||||
align-items: flex-end;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.value-selector {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { UiClockDateFormatSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-clock-date-format-picker";
|
||||
|
||||
@customElement("ha-selector-ui_clock_date_format")
|
||||
export class HaSelectorUiClockDateFormat extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: UiClockDateFormatSelector;
|
||||
|
||||
@property() public value?: string | string[];
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-clock-date-format-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
></ha-clock-date-format-picker>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-selector-ui_clock_date_format": HaSelectorUiClockDateFormat;
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,6 @@ const LOAD_ELEMENTS = {
|
||||
location: () => import("./ha-selector-location"),
|
||||
color_temp: () => import("./ha-selector-color-temp"),
|
||||
ui_action: () => import("./ha-selector-ui-action"),
|
||||
ui_clock_date_format: () => import("./ha-selector-ui-clock-date-format"),
|
||||
ui_color: () => import("./ha-selector-ui-color"),
|
||||
ui_state_content: () => import("./ha-selector-ui-state-content"),
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -21,6 +21,8 @@ export class HaTimeInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@property({ attribute: "auto-validate", type: Boolean }) autoValidate = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "enable-second" })
|
||||
public enableSecond = false;
|
||||
|
||||
@@ -71,6 +73,7 @@ export class HaTimeInput extends LitElement {
|
||||
.clearable=${this.clearable && this.value !== undefined}
|
||||
.helper=${this.helper}
|
||||
.placeholderLabels=${this.placeholderLabels}
|
||||
.autoValidate=${this.autoValidate}
|
||||
day-label="dd"
|
||||
hour-label="hh"
|
||||
min-label="mm"
|
||||
@@ -86,6 +89,7 @@ export class HaTimeInput extends LitElement {
|
||||
|
||||
const useAMPM = useAmPm(this.locale);
|
||||
let value: string | undefined;
|
||||
let updateHours = 0;
|
||||
|
||||
// An undefined eventValue means the time selector is being cleared,
|
||||
// the `value` variable will (intentionally) be left undefined.
|
||||
@@ -97,6 +101,8 @@ export class HaTimeInput extends LitElement {
|
||||
) {
|
||||
let hours = eventValue.hours || 0;
|
||||
if (eventValue && useAMPM) {
|
||||
updateHours =
|
||||
hours >= 12 && hours < 24 ? hours - 12 : hours === 0 ? 12 : 0;
|
||||
if (eventValue.amPm === "PM" && hours < 12) {
|
||||
hours += 12;
|
||||
}
|
||||
@@ -115,6 +121,17 @@ export class HaTimeInput extends LitElement {
|
||||
}`;
|
||||
}
|
||||
|
||||
if (updateHours) {
|
||||
// If the user entered a 24hr time in a 12hr input, we need to refresh the
|
||||
// input to ensure it resets back to the 12hr equivalent.
|
||||
this.updateComplete.then(() => {
|
||||
const input = this._input;
|
||||
if (input) {
|
||||
input.hours = updateHours;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (value === this.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import type WaPopover from "@home-assistant/webawesome/dist/components/popover/popover";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAssignedElements,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { popoverSupported } from "../common/feature-detect/support-popover";
|
||||
import { nextRender } from "../common/util/render-status";
|
||||
|
||||
export type ToastCloseReason =
|
||||
| "dismiss"
|
||||
@@ -19,23 +28,103 @@ export class HaToast extends LitElement {
|
||||
|
||||
@property({ type: Number, attribute: "timeout-ms" }) public timeoutMs = 4000;
|
||||
|
||||
@query("wa-popover")
|
||||
private _popover?: WaPopover;
|
||||
@query(".toast")
|
||||
private _toast?: HTMLDivElement;
|
||||
|
||||
@queryAssignedElements({ slot: "action", flatten: true })
|
||||
private _actionElements?: Element[];
|
||||
|
||||
@queryAssignedElements({ slot: "dismiss", flatten: true })
|
||||
private _dismissElements?: Element[];
|
||||
|
||||
@state() private _active = false;
|
||||
|
||||
@state() private _visible = false;
|
||||
|
||||
private _dismissTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _closeReason: ToastCloseReason = "programmatic";
|
||||
|
||||
private _transitionId = 0;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
clearTimeout(this._dismissTimer);
|
||||
this._transitionId += 1;
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
public async show(): Promise<void> {
|
||||
await this.updateComplete;
|
||||
await this._popover?.show();
|
||||
|
||||
clearTimeout(this._dismissTimer);
|
||||
|
||||
if (this._active && this._visible) {
|
||||
this._setDismissTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
const transitionId = ++this._transitionId;
|
||||
|
||||
this._active = true;
|
||||
await this.updateComplete;
|
||||
|
||||
if (transitionId !== this._transitionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showToastPopover();
|
||||
await nextRender();
|
||||
|
||||
if (transitionId !== this._transitionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._visible = true;
|
||||
await this.updateComplete;
|
||||
await this._waitForTransitionEnd();
|
||||
|
||||
if (transitionId !== this._transitionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setDismissTimer();
|
||||
}
|
||||
|
||||
public async hide(reason: ToastCloseReason = "programmatic"): Promise<void> {
|
||||
clearTimeout(this._dismissTimer);
|
||||
this._closeReason = reason;
|
||||
|
||||
if (!this._active) {
|
||||
return;
|
||||
}
|
||||
|
||||
const transitionId = ++this._transitionId;
|
||||
const wasVisible = this._visible;
|
||||
|
||||
this._visible = false;
|
||||
await this.updateComplete;
|
||||
|
||||
if (wasVisible) {
|
||||
await this._waitForTransitionEnd();
|
||||
}
|
||||
|
||||
if (transitionId !== this._transitionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._hideToastPopover();
|
||||
this._active = false;
|
||||
await this.updateComplete;
|
||||
|
||||
fireEvent(this, "toast-closed", {
|
||||
reason: this._closeReason,
|
||||
});
|
||||
this._closeReason = "programmatic";
|
||||
}
|
||||
|
||||
public close(reason: ToastCloseReason = "programmatic"): void {
|
||||
this.hide(reason);
|
||||
}
|
||||
|
||||
private _setDismissTimer() {
|
||||
if (this.timeoutMs > 0) {
|
||||
this._dismissTimer = setTimeout(() => {
|
||||
this.hide("timeout");
|
||||
@@ -43,96 +132,113 @@ export class HaToast extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public async hide(reason: ToastCloseReason = "programmatic"): Promise<void> {
|
||||
clearTimeout(this._dismissTimer);
|
||||
this._closeReason = reason;
|
||||
await this._popover?.hide();
|
||||
private _isPopoverOpen(): boolean {
|
||||
if (!this._toast || !popoverSupported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return this._toast.matches(":popover-open");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public close(reason: ToastCloseReason = "programmatic"): void {
|
||||
this.hide(reason);
|
||||
private _showToastPopover(): void {
|
||||
if (!this._toast || !popoverSupported || this._isPopoverOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toast.showPopover?.();
|
||||
}
|
||||
|
||||
private _handleAfterHide() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent<ToastClosedEventDetail>("toast-closed", {
|
||||
detail: { reason: this._closeReason },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
this._closeReason = "programmatic";
|
||||
private _hideToastPopover(): void {
|
||||
if (!this._toast || !popoverSupported || !this._isPopoverOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._toast.hidePopover?.();
|
||||
}
|
||||
|
||||
private async _waitForTransitionEnd(): Promise<void> {
|
||||
const toastEl = this._toast;
|
||||
if (!toastEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const animations = toastEl.getAnimations({ subtree: true });
|
||||
if (animations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.allSettled(animations.map((animation) => animation.finished));
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const hasAction =
|
||||
(this._actionElements?.length ?? 0) > 0 ||
|
||||
(this._dismissElements?.length ?? 0) > 0;
|
||||
|
||||
return html`
|
||||
<div id="toast-anchor" aria-hidden="true"></div>
|
||||
<wa-popover
|
||||
for="toast-anchor"
|
||||
placement="top"
|
||||
distance="16"
|
||||
skidding="0"
|
||||
without-arrow
|
||||
@wa-after-hide=${this._handleAfterHide}
|
||||
<div
|
||||
class=${classMap({
|
||||
toast: true,
|
||||
active: this._active,
|
||||
visible: this._visible,
|
||||
})}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
popover=${ifDefined(popoverSupported ? "manual" : undefined)}
|
||||
>
|
||||
<div class="toast" role="status" aria-live="polite">
|
||||
<span class="message">${this.labelText}</span>
|
||||
<div class="actions">
|
||||
<slot name="action"></slot>
|
||||
<slot name="dismiss"></slot>
|
||||
</div>
|
||||
<span class="message">${this.labelText}</span>
|
||||
<div class=${classMap({ actions: true, "has-action": hasAction })}>
|
||||
<slot name="action"></slot>
|
||||
<slot name="dismiss"></slot>
|
||||
</div>
|
||||
</wa-popover>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
#toast-anchor {
|
||||
position: fixed;
|
||||
bottom: calc(8px + var(--safe-area-inset-bottom));
|
||||
inset-inline-start: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
wa-popover {
|
||||
--arrow-size: 0;
|
||||
--max-width: min(
|
||||
650px,
|
||||
calc(
|
||||
100vw -
|
||||
16px - var(--safe-area-inset-left) - var(--safe-area-inset-right)
|
||||
)
|
||||
);
|
||||
--show-duration: var(--ha-animation-duration-fast, 150ms);
|
||||
--hide-duration: var(--ha-animation-duration-fast, 150ms);
|
||||
}
|
||||
|
||||
wa-popover::part(body) {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toast {
|
||||
box-sizing: border-box;
|
||||
min-width: min(
|
||||
350px,
|
||||
calc(
|
||||
100vw -
|
||||
16px - var(--safe-area-inset-left) - var(--safe-area-inset-right)
|
||||
)
|
||||
position: fixed;
|
||||
inset-block-start: auto;
|
||||
inset-inline-end: auto;
|
||||
inset-block-end: calc(
|
||||
var(--safe-area-inset-bottom, 0px) + var(--ha-space-4)
|
||||
);
|
||||
max-width: 650px;
|
||||
inset-inline-start: 50%;
|
||||
margin: 0;
|
||||
width: max-content;
|
||||
height: auto;
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
min-width: min(350px, calc(var(--safe-width) - var(--ha-space-4)));
|
||||
max-width: min(650px, var(--safe-width));
|
||||
min-height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-2) var(--ha-space-3);
|
||||
color: var(--inverse-primary-text-color);
|
||||
background-color: var(--inverse-surface-color);
|
||||
padding: var(--ha-space-3) var(--ha-space-4);
|
||||
color: var(--ha-color-on-neutral-loud);
|
||||
background-color: var(--ha-color-neutral-10);
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
box-shadow: var(--wa-shadow-l);
|
||||
opacity: 0;
|
||||
transform: translate(-50%, var(--ha-space-2));
|
||||
transition:
|
||||
opacity var(--ha-animation-duration-fast, 150ms) ease,
|
||||
transform var(--ha-animation-duration-fast, 150ms) ease;
|
||||
}
|
||||
|
||||
.toast:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toast.visible {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.message {
|
||||
@@ -144,23 +250,26 @@ export class HaToast extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
color: var(--ha-color-on-neutral-loud);
|
||||
}
|
||||
|
||||
.actions:not(.has-action) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
.toast {
|
||||
min-width: calc(
|
||||
100vw - var(--safe-area-inset-left) - var(--safe-area-inset-right)
|
||||
);
|
||||
border-radius: 0;
|
||||
min-width: var(--safe-width);
|
||||
max-width: var(--safe-width);
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementEventMap {
|
||||
"toast-closed": CustomEvent<ToastClosedEventDetail>;
|
||||
interface HASSDomEvents {
|
||||
"toast-closed": ToastClosedEventDetail;
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
|
||||
@@ -11,6 +11,27 @@ import "../ha-svg-icon";
|
||||
import "./ha-input";
|
||||
import type { HaInput, InputType } from "./ha-input";
|
||||
|
||||
/**
|
||||
* Home Assistant input with copy button
|
||||
*
|
||||
* @element ha-input-copy
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A read-only input component with a copy-to-clipboard button.
|
||||
* Supports optional value masking with a toggle to reveal the hidden value.
|
||||
*
|
||||
* @attr {string} value - The value to display and copy.
|
||||
* @attr {string} masked-value - An alternative masked display value (for example, "••••••").
|
||||
* @attr {string} label - Label for the copy button. Defaults to the localized "Copy" text.
|
||||
* @attr {boolean} readonly - Makes the inner input readonly.
|
||||
* @attr {boolean} disabled - Disables the inner input.
|
||||
* @attr {boolean} masked-toggle - Shows a toggle button to reveal/hide the masked value.
|
||||
* @attr {("text"|"password"|"email"|"number"|"tel"|"url"|"search"|"date"|"datetime-local"|"time"|"color")} type - Sets the input type.
|
||||
* @attr {string} placeholder - Placeholder text for the input.
|
||||
* @attr {string} validation-message - Custom validation message.
|
||||
* @attr {boolean} auto-validate - Validates the input on blur.
|
||||
*/
|
||||
@customElement("ha-input-copy")
|
||||
export class HaInputCopy extends LitElement {
|
||||
@property({ attribute: "value" }) public value!: string;
|
||||
|
||||
@@ -14,6 +14,23 @@ import "../ha-sortable";
|
||||
import "./ha-input";
|
||||
import type { HaInput, InputType } from "./ha-input";
|
||||
|
||||
/**
|
||||
* Home Assistant multi-value input component
|
||||
*
|
||||
* @element ha-input-multi
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A dynamic list of text inputs that allows adding, removing, and optionally reordering values.
|
||||
* Useful for managing arrays of strings such as URLs, tags, or other repeated text values.
|
||||
*
|
||||
* @attr {boolean} disabled - Disables all inputs and buttons.
|
||||
* @attr {boolean} sortable - Enables drag-and-drop reordering of items.
|
||||
* @attr {boolean} item-index - Appends a 1-based index number to each item's label.
|
||||
* @attr {boolean} update-on-blur - Fires value-changed on blur instead of on input.
|
||||
*
|
||||
* @fires value-changed - Fired when the list of values changes. `event.detail.value` contains the new string array.
|
||||
*/
|
||||
@customElement("ha-input-multi")
|
||||
class HaInputMulti extends LitElement {
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
@@ -22,19 +39,19 @@ class HaInputMulti extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public helper?: string;
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ attribute: false }) public inputType?: InputType;
|
||||
@property({ attribute: "input-type" }) public inputType?: InputType;
|
||||
|
||||
@property({ attribute: false }) public inputSuffix?: string;
|
||||
@property({ attribute: "input-suffix" }) public inputSuffix?: string;
|
||||
|
||||
@property({ attribute: false }) public inputPrefix?: string;
|
||||
@property({ attribute: "input-prefix" }) public inputPrefix?: string;
|
||||
|
||||
@property({ attribute: false }) public autocomplete?: string;
|
||||
@property() public autocomplete?: string;
|
||||
|
||||
@property({ attribute: false }) public addLabel?: string;
|
||||
@property({ attribute: "add-label" }) public addLabel?: string;
|
||||
|
||||
@property({ attribute: false }) public removeLabel?: string;
|
||||
@property({ attribute: "remove-label" }) public removeLabel?: string;
|
||||
|
||||
@property({ attribute: "item-index", type: Boolean })
|
||||
public itemIndex = false;
|
||||
|
||||
51
src/components/input/ha-input-search.ts
Normal file
51
src/components/input/ha-input-search.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
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 { HaInput } from "./ha-input";
|
||||
|
||||
/**
|
||||
* Home Assistant search input component
|
||||
*
|
||||
* @element ha-input-search
|
||||
* @extends {HaInput}
|
||||
*
|
||||
* @summary
|
||||
* A pre-configured search input that extends `ha-input` with a magnify icon, clear button,
|
||||
* and a localized "Search" placeholder. Autocomplete is disabled by default.
|
||||
*/
|
||||
@customElement("ha-input-search")
|
||||
export class HaInputSearch extends HaInput {
|
||||
@state()
|
||||
@consume({ context: localizeContext, subscribe: true })
|
||||
private localize!: ContextType<typeof localizeContext>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.withClear = true;
|
||||
this.autocomplete = this.autocomplete || "off";
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
!this.label &&
|
||||
!this.placeholder &&
|
||||
(!this.hasUpdated || changedProps.has("localize"))
|
||||
) {
|
||||
this.placeholder = this.localize("ui.common.search");
|
||||
}
|
||||
}
|
||||
|
||||
protected renderStartDefault() {
|
||||
return html`<ha-svg-icon slot="start" .path=${mdiMagnify}></ha-svg-icon>`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-input-search": HaInputSearch;
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,21 @@ import "@home-assistant/webawesome/dist/components/input/input";
|
||||
import type WaInput from "@home-assistant/webawesome/dist/components/input/input";
|
||||
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
|
||||
import { mdiClose, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { LitElement, type PropertyValues, css, html, nothing } from "lit";
|
||||
import {
|
||||
LitElement,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } 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";
|
||||
|
||||
export type InputType =
|
||||
@@ -22,20 +30,62 @@ export type InputType =
|
||||
| "tel"
|
||||
| "text"
|
||||
| "time"
|
||||
| "color"
|
||||
| "url";
|
||||
|
||||
/**
|
||||
* Home Assistant input component
|
||||
*
|
||||
* @element ha-input
|
||||
* @extends {LitElement}
|
||||
*
|
||||
* @summary
|
||||
* A text input component supporting Home Assistant theming and validation, based on webawesome input.
|
||||
* Supports multiple input types including text, number, password, email, search, and more.
|
||||
*
|
||||
* @slot start - Content placed before the input (usually for icons or prefixes).
|
||||
* @slot end - Content placed after the input (usually for icons or suffixes).
|
||||
* @slot label - Custom label content. Overrides the `label` property.
|
||||
* @slot hint - Custom hint content. Overrides the `hint` property.
|
||||
* @slot clear-icon - Custom clear icon. Defaults to a close icon button.
|
||||
* @slot show-password-icon - Custom show password icon. Defaults to an eye icon button.
|
||||
* @slot hide-password-icon - Custom hide password icon. Defaults to an eye-off icon button.
|
||||
*
|
||||
* @csspart wa-base - The underlying wa-input base wrapper.
|
||||
* @csspart wa-hint - The underlying wa-input hint container.
|
||||
* @csspart wa-input - The underlying wa-input input element.
|
||||
*
|
||||
* @cssprop --ha-input-padding-top - Padding above the input.
|
||||
* @cssprop --ha-input-padding-bottom - Padding below the input. Defaults to `var(--ha-space-2)`.
|
||||
* @cssprop --ha-input-text-align - Text alignment of the input. Defaults to `start`.
|
||||
* @cssprop --ha-input-required-marker - The marker shown after the label for required fields. Defaults to `"*"`.
|
||||
*
|
||||
* @attr {("material"|"outlined")} appearance - Sets the input appearance style. "material" is the default filled style, "outlined" uses a bordered style.
|
||||
* @attr {("date"|"datetime-local"|"email"|"number"|"password"|"search"|"tel"|"text"|"time"|"color"|"url")} type - Sets the input type.
|
||||
* @attr {string} label - The input's label text.
|
||||
* @attr {string} hint - The input's hint/helper text.
|
||||
* @attr {string} placeholder - Placeholder text shown when the input is empty.
|
||||
* @attr {boolean} with-clear - Adds a clear button when the input is not empty.
|
||||
* @attr {boolean} readonly - Makes the input readonly.
|
||||
* @attr {boolean} disabled - Disables the input and prevents user interaction.
|
||||
* @attr {boolean} required - Makes the input a required field.
|
||||
* @attr {boolean} password-toggle - Adds a button to toggle the password visibility.
|
||||
* @attr {boolean} without-spin-buttons - Hides the browser's built-in spin buttons for number inputs.
|
||||
* @attr {boolean} auto-validate - Validates the input on blur instead of on form submit.
|
||||
* @attr {boolean} invalid - Marks the input as invalid.
|
||||
* @attr {boolean} inset-label - Uses an inset label style where the label stays inside the input.
|
||||
* @attr {string} validation-message - Custom validation message shown when the input is invalid.
|
||||
*/
|
||||
@customElement("ha-input")
|
||||
export class HaInput extends LitElement {
|
||||
@property({ reflect: true }) appearance: "material" | "outlined" = "material";
|
||||
|
||||
@property({ reflect: true })
|
||||
public type: InputType = "text";
|
||||
|
||||
@property()
|
||||
public value?: string;
|
||||
|
||||
/** Draws a pill-style input with rounded edges. */
|
||||
@property({ type: Boolean })
|
||||
public pill = false;
|
||||
|
||||
/** The input's label. */
|
||||
@property()
|
||||
public label = "";
|
||||
@@ -296,7 +346,9 @@ export class HaInput extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
class=${classMap({
|
||||
invalid: this.invalid || this._invalid,
|
||||
"label-raised": this.value || (this.label && this.placeholder),
|
||||
"label-raised":
|
||||
(this.value !== undefined && this.value !== "") ||
|
||||
(this.label && this.placeholder),
|
||||
"no-label": !this.label,
|
||||
"hint-hidden":
|
||||
!this.hint &&
|
||||
@@ -316,12 +368,10 @@ export class HaInput extends LitElement {
|
||||
>${this._renderLabel(this.label, this.required)}</slot
|
||||
>`
|
||||
: nothing}
|
||||
<slot
|
||||
name="start"
|
||||
slot="start"
|
||||
@slotchange=${this._syncStartSlotWidth}
|
||||
></slot>
|
||||
<slot name="end" slot="end"></slot>
|
||||
<slot name="start" slot="start" @slotchange=${this._syncStartSlotWidth}>
|
||||
${this.renderStartDefault()}
|
||||
</slot>
|
||||
<slot name="end" slot="end"> ${this.renderEndDefault()} </slot>
|
||||
<slot name="clear-icon" slot="clear-icon">
|
||||
<ha-icon-button .path=${mdiClose}></ha-icon-button>
|
||||
</slot>
|
||||
@@ -354,6 +404,14 @@ export class HaInput extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
protected renderStartDefault(): TemplateResult | typeof nothing {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
protected renderEndDefault(): TemplateResult | typeof nothing {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _handleInput() {
|
||||
this.value = this._input?.value ?? undefined;
|
||||
if (this._invalid && this._input?.checkValidity()) {
|
||||
@@ -427,6 +485,9 @@ export class HaInput extends LitElement {
|
||||
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;
|
||||
@@ -452,7 +513,7 @@ export class HaInput extends LitElement {
|
||||
font-size: var(--ha-font-size-m);
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-input::part(label) {
|
||||
:host([appearance="material"]:focus-within) wa-input::part(label) {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
@@ -462,7 +523,8 @@ export class HaInput extends LitElement {
|
||||
}
|
||||
|
||||
wa-input.label-raised::part(label),
|
||||
:host(:focus-within) wa-input::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);
|
||||
}
|
||||
@@ -480,7 +542,19 @@ export class HaInput extends LitElement {
|
||||
transition: background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
wa-input::part(base)::after {
|
||||
:host([appearance="outlined"]) wa-input.no-label::part(base) {
|
||||
height: 32px;
|
||||
padding: 0 var(--ha-space-2);
|
||||
}
|
||||
|
||||
: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="material"]) ::part(base)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -493,13 +567,15 @@ export class HaInput extends LitElement {
|
||||
background-color var(--wa-transition-normal) ease-in-out;
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-input::part(base)::after {
|
||||
:host([appearance="material"]:focus-within) wa-input::part(base)::after {
|
||||
height: 2px;
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
:host(:focus-within) wa-input.invalid::part(base)::after,
|
||||
wa-input.invalid:not([disabled])::part(base)::after {
|
||||
: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);
|
||||
}
|
||||
|
||||
@@ -507,11 +583,19 @@ export class HaInput extends LitElement {
|
||||
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);
|
||||
@@ -525,10 +609,21 @@ export class HaInput extends LitElement {
|
||||
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;
|
||||
@@ -550,6 +645,11 @@ export class HaInput extends LitElement {
|
||||
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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-outlined-text-field";
|
||||
import type { HaOutlinedTextField } from "./ha-outlined-text-field";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("search-input-outlined")
|
||||
class SearchInputOutlined extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public filter?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public suffix = false;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@property({ type: String })
|
||||
public placeholder?: string;
|
||||
|
||||
public focus() {
|
||||
this._input?.focus();
|
||||
}
|
||||
|
||||
@query("ha-outlined-text-field", true) private _input!: HaOutlinedTextField;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const placeholder =
|
||||
this.placeholder || this.hass.localize("ui.common.search");
|
||||
|
||||
return html`
|
||||
<ha-outlined-text-field
|
||||
.autofocus=${this.autofocus}
|
||||
.aria-label=${this.label || this.hass.localize("ui.common.search")}
|
||||
.placeholder=${placeholder}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
.iconTrailing=${this.filter || this.suffix}
|
||||
@input=${this._filterInputChanged}
|
||||
dense
|
||||
>
|
||||
<slot name="prefix" slot="leading-icon">
|
||||
<ha-svg-icon
|
||||
tabindex="-1"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
</slot>
|
||||
${this.filter
|
||||
? html`<ha-icon-button
|
||||
aria-label="Clear input"
|
||||
slot="trailing-icon"
|
||||
@click=${this._clearSearch}
|
||||
.path=${mdiClose}
|
||||
>
|
||||
</ha-icon-button>`
|
||||
: nothing}
|
||||
</ha-outlined-text-field>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(value: string) {
|
||||
fireEvent(this, "value-changed", { value: String(value) });
|
||||
}
|
||||
|
||||
private async _filterInputChanged(e) {
|
||||
this._filterChanged(e.target.value);
|
||||
}
|
||||
|
||||
private async _clearSearch() {
|
||||
this._filterChanged("");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
/* For iOS */
|
||||
z-index: 0;
|
||||
}
|
||||
ha-outlined-text-field {
|
||||
display: block;
|
||||
width: 100%;
|
||||
--ha-outlined-field-container-color: var(--card-background-color);
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-button {
|
||||
--ha-icon-button-size: 24px;
|
||||
height: var(--ha-icon-button-size);
|
||||
display: flex;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-svg-icon {
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"search-input-outlined": SearchInputOutlined;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import { mdiClose, mdiMagnify } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("search-input")
|
||||
class SearchInput extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public filter?: string;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public suffix = false;
|
||||
|
||||
// eslint-disable-next-line lit/no-native-attributes
|
||||
@property({ type: Boolean }) public autofocus = false;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
public focus() {
|
||||
this._input?.focus();
|
||||
}
|
||||
|
||||
@query("ha-textfield", true) private _input!: HaTextField;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-textfield
|
||||
.autofocus=${this.autofocus}
|
||||
autocomplete="off"
|
||||
.label=${this.label || this.hass.localize("ui.common.search")}
|
||||
.value=${this.filter || ""}
|
||||
icon
|
||||
.iconTrailing=${this.filter || this.suffix}
|
||||
@input=${this._filterInputChanged}
|
||||
>
|
||||
<slot name="prefix" slot="leadingIcon">
|
||||
<ha-svg-icon
|
||||
tabindex="-1"
|
||||
class="prefix"
|
||||
.path=${mdiMagnify}
|
||||
></ha-svg-icon>
|
||||
</slot>
|
||||
<div class="trailing" slot="trailingIcon">
|
||||
${this.filter &&
|
||||
html`
|
||||
<ha-icon-button
|
||||
@click=${this._clearSearch}
|
||||
.label=${this.hass.localize("ui.common.clear")}
|
||||
.path=${mdiClose}
|
||||
class="clear-button"
|
||||
></ha-icon-button>
|
||||
`}
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
</ha-textfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(value: string) {
|
||||
fireEvent(this, "value-changed", { value: String(value) });
|
||||
}
|
||||
|
||||
private async _filterInputChanged(e) {
|
||||
this._filterChanged(e.target.value);
|
||||
}
|
||||
|
||||
private async _clearSearch() {
|
||||
this._filterChanged("");
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
}
|
||||
ha-svg-icon,
|
||||
ha-icon-button {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-svg-icon {
|
||||
outline: none;
|
||||
}
|
||||
.clear-button {
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: inherit;
|
||||
}
|
||||
.trailing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"search-input": SearchInput;
|
||||
}
|
||||
}
|
||||
@@ -535,7 +535,7 @@ export class HaTargetPickerItemRow extends LitElement {
|
||||
|
||||
const stateObject: HassEntity | undefined = this.hass.states[item];
|
||||
const entityName = stateObject
|
||||
? computeEntityName(stateObject, this.hass.entities)
|
||||
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
|
||||
: item;
|
||||
const { area, device } = stateObject
|
||||
? getEntityContext(
|
||||
|
||||
@@ -78,6 +78,19 @@ const localizeTimeString = (
|
||||
}
|
||||
};
|
||||
|
||||
const formatNumericLimitValue = (
|
||||
hass: HomeAssistant,
|
||||
value?: number | string
|
||||
) => {
|
||||
if (typeof value !== "string" || !isValidEntityId(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return hass.states[value]
|
||||
? computeStateName(hass.states[value]) || value
|
||||
: value;
|
||||
};
|
||||
|
||||
export const describeTrigger = (
|
||||
trigger: Trigger,
|
||||
hass: HomeAssistant,
|
||||
@@ -233,8 +246,8 @@ const describeLegacyTrigger = (
|
||||
attribute: attribute,
|
||||
entity: formatListWithOrs(hass.locale, entities),
|
||||
numberOfEntities: entities.length,
|
||||
above: trigger.above,
|
||||
below: trigger.below,
|
||||
above: formatNumericLimitValue(hass, trigger.above),
|
||||
below: formatNumericLimitValue(hass, trigger.below),
|
||||
duration: duration,
|
||||
}
|
||||
);
|
||||
@@ -246,7 +259,7 @@ const describeLegacyTrigger = (
|
||||
attribute: attribute,
|
||||
entity: formatListWithOrs(hass.locale, entities),
|
||||
numberOfEntities: entities.length,
|
||||
above: trigger.above,
|
||||
above: formatNumericLimitValue(hass, trigger.above),
|
||||
duration: duration,
|
||||
}
|
||||
);
|
||||
@@ -258,7 +271,7 @@ const describeLegacyTrigger = (
|
||||
attribute: attribute,
|
||||
entity: formatListWithOrs(hass.locale, entities),
|
||||
numberOfEntities: entities.length,
|
||||
below: trigger.below,
|
||||
below: formatNumericLimitValue(hass, trigger.below),
|
||||
duration: duration,
|
||||
}
|
||||
);
|
||||
@@ -1116,8 +1129,8 @@ const describeLegacyCondition = (
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
above: condition.above,
|
||||
below: condition.below,
|
||||
above: formatNumericLimitValue(hass, condition.above),
|
||||
below: formatNumericLimitValue(hass, condition.below),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1128,7 +1141,7 @@ const describeLegacyCondition = (
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
above: condition.above,
|
||||
above: formatNumericLimitValue(hass, condition.above),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1139,7 +1152,7 @@ const describeLegacyCondition = (
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
below: condition.below,
|
||||
below: formatNumericLimitValue(hass, condition.below),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -464,6 +464,15 @@ export const convertStatisticsToHistory = (
|
||||
return statisticsHistory;
|
||||
};
|
||||
|
||||
export const limitedHistoryFromStateObj = (
|
||||
state: HassEntity
|
||||
): EntityHistoryState[] => [
|
||||
{
|
||||
s: state.state,
|
||||
a: state.attributes,
|
||||
lu: new Date(state.last_updated).getTime() / 1000,
|
||||
},
|
||||
];
|
||||
export const computeHistory = (
|
||||
hass: HomeAssistant,
|
||||
stateHistory: HistoryStates,
|
||||
@@ -484,13 +493,9 @@ export const computeHistory = (
|
||||
if (entity in stateHistory) {
|
||||
localStateHistory[entity] = stateHistory[entity];
|
||||
} else if (hass.states[entity]) {
|
||||
localStateHistory[entity] = [
|
||||
{
|
||||
s: hass.states[entity].state,
|
||||
a: hass.states[entity].attributes,
|
||||
lu: new Date(hass.states[entity].last_updated).getTime() / 1000,
|
||||
},
|
||||
];
|
||||
localStateHistory[entity] = limitedHistoryFromStateObj(
|
||||
hass.states[entity]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ import {
|
||||
mdiAirFilter,
|
||||
mdiAlert,
|
||||
mdiAppleSafari,
|
||||
mdiBattery,
|
||||
mdiBell,
|
||||
mdiBookmark,
|
||||
mdiBrightness6,
|
||||
mdiBullhorn,
|
||||
mdiButtonPointer,
|
||||
mdiCalendar,
|
||||
@@ -16,19 +18,26 @@ import {
|
||||
mdiCog,
|
||||
mdiCommentAlert,
|
||||
mdiCounter,
|
||||
mdiDoorOpen,
|
||||
mdiEye,
|
||||
mdiFlash,
|
||||
mdiFlower,
|
||||
mdiFormatListBulleted,
|
||||
mdiFormTextbox,
|
||||
mdiForumOutline,
|
||||
mdiGarageOpen,
|
||||
mdiGate,
|
||||
mdiGoogleAssistant,
|
||||
mdiGoogleCirclesCommunities,
|
||||
mdiHomeAccount,
|
||||
mdiHomeAutomation,
|
||||
mdiImage,
|
||||
mdiImageFilterFrames,
|
||||
mdiLedOn,
|
||||
mdiLightbulb,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMicrophoneMessage,
|
||||
mdiMotionSensor,
|
||||
mdiPalette,
|
||||
mdiRayVertex,
|
||||
mdiRemote,
|
||||
@@ -40,10 +49,14 @@ import {
|
||||
mdiSpeakerMessage,
|
||||
mdiStarFourPoints,
|
||||
mdiThermostat,
|
||||
mdiThermometer,
|
||||
mdiTimerOutline,
|
||||
mdiToggleSwitch,
|
||||
mdiWater,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherPartlyCloudy,
|
||||
mdiWhiteBalanceSunny,
|
||||
mdiWindowClosed,
|
||||
} from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
@@ -74,6 +87,7 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
air_quality: mdiAirFilter,
|
||||
alert: mdiAlert,
|
||||
automation: mdiRobot,
|
||||
battery: mdiBattery,
|
||||
calendar: mdiCalendar,
|
||||
climate: mdiThermostat,
|
||||
configurator: mdiCog,
|
||||
@@ -83,12 +97,18 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
datetime: mdiCalendarClock,
|
||||
demo: mdiHomeAssistant,
|
||||
device_tracker: mdiAccount,
|
||||
door: mdiDoorOpen,
|
||||
garage_door: mdiGarageOpen,
|
||||
gate: mdiGate,
|
||||
google_assistant: mdiGoogleAssistant,
|
||||
group: mdiGoogleCirclesCommunities,
|
||||
homeassistant: mdiHomeAssistant,
|
||||
homekit: mdiHomeAutomation,
|
||||
humidity: mdiWaterPercent,
|
||||
illuminance: mdiBrightness6,
|
||||
image_processing: mdiImageFilterFrames,
|
||||
image: mdiImage,
|
||||
infrared: mdiLedOn,
|
||||
input_boolean: mdiToggleSwitch,
|
||||
input_button: mdiButtonPointer,
|
||||
input_datetime: mdiCalendarClock,
|
||||
@@ -97,11 +117,15 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
input_text: mdiFormTextbox,
|
||||
lawn_mower: mdiRobotMower,
|
||||
light: mdiLightbulb,
|
||||
moisture: mdiWater,
|
||||
motion: mdiMotionSensor,
|
||||
notify: mdiCommentAlert,
|
||||
number: mdiRayVertex,
|
||||
occupancy: mdiHomeAccount,
|
||||
persistent_notification: mdiBell,
|
||||
person: mdiAccount,
|
||||
plant: mdiFlower,
|
||||
power: mdiFlash,
|
||||
proximity: mdiAppleSafari,
|
||||
remote: mdiRemote,
|
||||
scene: mdiPalette,
|
||||
@@ -113,6 +137,7 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
siren: mdiBullhorn,
|
||||
stt: mdiMicrophoneMessage,
|
||||
sun: mdiWhiteBalanceSunny,
|
||||
temperature: mdiThermometer,
|
||||
text: mdiFormTextbox,
|
||||
time: mdiClock,
|
||||
timer: mdiTimerOutline,
|
||||
@@ -122,6 +147,7 @@ export const FALLBACK_DOMAIN_ICONS = {
|
||||
vacuum: mdiRobotVacuum,
|
||||
wake_word: mdiChatSleep,
|
||||
weather: mdiWeatherPartlyCloudy,
|
||||
window: mdiWindowClosed,
|
||||
zone: mdiMapMarkerRadius,
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ export const SCENE_IGNORED_DOMAINS = [
|
||||
"device_tracker",
|
||||
"event",
|
||||
"image_processing",
|
||||
"infrared",
|
||||
"input_button",
|
||||
"persistent_notification",
|
||||
"person",
|
||||
|
||||
@@ -22,6 +22,8 @@ import type {
|
||||
} from "./entity/entity_registry";
|
||||
import type { EntitySources } from "./entity/entity_sources";
|
||||
|
||||
export type ThresholdMode = "crossed" | "changed" | "is";
|
||||
|
||||
export type Selector =
|
||||
| ActionSelector
|
||||
| AddonSelector
|
||||
@@ -75,7 +77,6 @@ export type Selector =
|
||||
| TTSSelector
|
||||
| TTSVoiceSelector
|
||||
| UiActionSelector
|
||||
| UiClockDateFormatSelector
|
||||
| UiColorSelector
|
||||
| UiStateContentSelector
|
||||
| BackupLocationSelector;
|
||||
@@ -367,6 +368,7 @@ export interface NumberSelector {
|
||||
|
||||
export interface NumericThresholdSelector {
|
||||
numeric_threshold: {
|
||||
mode?: ThresholdMode;
|
||||
unit_of_measurement?: readonly string[];
|
||||
number?: NumberSelector["number"];
|
||||
entity?: EntitySelectorFilter | readonly EntitySelectorFilter[];
|
||||
@@ -517,10 +519,6 @@ export interface UiActionSelector {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface UiClockDateFormatSelector {
|
||||
ui_clock_date_format: {} | null;
|
||||
}
|
||||
|
||||
export interface UiColorExtraOption {
|
||||
value: string;
|
||||
label: string;
|
||||
|
||||
@@ -16,6 +16,7 @@ import "../../../components/ha-humidifier-state";
|
||||
import "../../../components/ha-select";
|
||||
import "../../../components/ha-slider";
|
||||
import "../../../components/ha-time-input";
|
||||
import "../../../components/input/ha-input";
|
||||
import { isTiltOnly } from "../../../data/cover";
|
||||
import { isUnavailableState } from "../../../data/entity/entity";
|
||||
import type { ImageEntity } from "../../../data/image";
|
||||
@@ -72,7 +73,7 @@ class EntityPreviewRow extends LitElement {
|
||||
min-width: 45px;
|
||||
text-align: end;
|
||||
}
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
text-align: end;
|
||||
direction: ltr !important;
|
||||
}
|
||||
@@ -273,18 +274,23 @@ class EntityPreviewRow extends LitElement {
|
||||
</span>
|
||||
</div>
|
||||
`
|
||||
: html` <div class="numberflex numberstate">
|
||||
<ha-textfield
|
||||
autoValidate
|
||||
: html`<div class="numberflex numberstate">
|
||||
<ha-input
|
||||
auto-validate
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
pattern="[0-9]+([\\.][0-9]+)?"
|
||||
.step=${Number(stateObj.attributes.step)}
|
||||
.min=${Number(stateObj.attributes.min)}
|
||||
.max=${Number(stateObj.attributes.max)}
|
||||
.value=${stateObj.state}
|
||||
.suffix=${stateObj.attributes.unit_of_measurement}
|
||||
type="number"
|
||||
></ha-textfield>
|
||||
>
|
||||
${stateObj.attributes.unit_of_measurement
|
||||
? html`<span slot="end"
|
||||
>${stateObj.attributes.unit_of_measurement}</span
|
||||
>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
</div>`}
|
||||
`;
|
||||
}
|
||||
@@ -323,7 +329,7 @@ class EntityPreviewRow extends LitElement {
|
||||
|
||||
if (domain === "text") {
|
||||
return html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.label=${computeStateName(stateObj)}
|
||||
.disabled=${isUnavailableState(stateObj.state)}
|
||||
.value=${stateObj.state}
|
||||
@@ -333,7 +339,7 @@ class EntityPreviewRow extends LitElement {
|
||||
.pattern=${stateObj.attributes.pattern}
|
||||
.type=${stateObj.attributes.mode}
|
||||
placeholder=${this.hass!.localize("ui.card.text.emtpy_value")}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import "../../components/ha-area-picker";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HaInput } from "../../components/input/ha-input";
|
||||
import { assistSatelliteSupportsSetupFlow } from "../../data/assist_satellite";
|
||||
import { getConfigEntries } from "../../data/config_entries";
|
||||
import type { DataEntryFlowStepCreateEntry } from "../../data/data_entry_flow";
|
||||
@@ -22,7 +24,7 @@ import {
|
||||
type EntityRegistryDisplayEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import { brandsUrl } from "../../util/brands-url";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog";
|
||||
@@ -162,7 +164,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.label=${localize(
|
||||
"ui.panel.config.integrations.config_flow.device_name"
|
||||
)}
|
||||
@@ -174,7 +176,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
computeDeviceName(device)}
|
||||
@change=${this._deviceNameChanged}
|
||||
.device=${device.id}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.device=${device.id}
|
||||
@@ -278,7 +280,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _areaPicked(ev: CustomEvent) {
|
||||
private async _areaPicked(ev: ValueChangedEvent<string>) {
|
||||
const picker = ev.currentTarget as any;
|
||||
const device = picker.device;
|
||||
const area = ev.detail.value;
|
||||
@@ -290,9 +292,9 @@ class StepFlowCreateEntry extends LitElement {
|
||||
this.requestUpdate("_deviceUpdate");
|
||||
}
|
||||
|
||||
private _deviceNameChanged(ev): void {
|
||||
const picker = ev.currentTarget as any;
|
||||
const device = picker.device;
|
||||
private _deviceNameChanged(ev: InputEvent): void {
|
||||
const picker = ev.currentTarget as HaInput;
|
||||
const device = (picker as any).device;
|
||||
const name = picker.value;
|
||||
|
||||
if (!(device in this._deviceUpdate)) {
|
||||
@@ -343,12 +345,11 @@ class StepFlowCreateEntry extends LitElement {
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-textfield,
|
||||
ha-area-picker {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield {
|
||||
margin: 8px 0;
|
||||
ha-input {
|
||||
margin: var(--ha-space-2) 0;
|
||||
}
|
||||
.buttons > *:last-child {
|
||||
margin-left: auto;
|
||||
|
||||
@@ -11,6 +11,8 @@ export const DialogMixin = <
|
||||
superClass: T
|
||||
) =>
|
||||
class extends superClass implements HassDialogNext<P> {
|
||||
public dialogNext = true as const;
|
||||
|
||||
declare public params?: P;
|
||||
|
||||
private _closePromise?: Promise<boolean>;
|
||||
|
||||
@@ -5,10 +5,10 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-control-button";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-dialog";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HaInput } from "../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HassDialog } from "../make-dialog-manager";
|
||||
import type { EnterCodeDialogParams } from "./show-enter-code-dialog";
|
||||
@@ -39,7 +39,7 @@ export class DialogEnterCode
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@query("#code") private _input?: HaTextField;
|
||||
@query("#code") private _input?: HaInput;
|
||||
|
||||
@state() private _showClearButton = false;
|
||||
|
||||
@@ -97,7 +97,7 @@ export class DialogEnterCode
|
||||
}
|
||||
|
||||
private _inputValueChange(e) {
|
||||
const field = e.currentTarget as HaTextField;
|
||||
const field = e.currentTarget as HaInput;
|
||||
const val = field.value;
|
||||
this._showClearButton = !!val;
|
||||
}
|
||||
@@ -119,7 +119,7 @@ export class DialogEnterCode
|
||||
width="small"
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
class="input"
|
||||
?autofocus=${!this._narrow}
|
||||
id="code"
|
||||
@@ -129,7 +129,7 @@ export class DialogEnterCode
|
||||
validateOnInitialRender
|
||||
pattern=${ifDefined(this._dialogParams.codePattern)}
|
||||
inputmode="text"
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@@ -157,14 +157,15 @@ export class DialogEnterCode
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div class="container">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
@input=${this._inputValueChange}
|
||||
id="code"
|
||||
.label=${this.hass.localize("ui.dialogs.enter_code.input_label")}
|
||||
type="password"
|
||||
inputmode="numeric"
|
||||
?autofocus=${!this._narrow}
|
||||
></ha-textfield>
|
||||
password-toggle
|
||||
></ha-input>
|
||||
<div class="keypad">
|
||||
${BUTTONS.map((value) =>
|
||||
value === ""
|
||||
@@ -212,7 +213,7 @@ export class DialogEnterCode
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-textfield";
|
||||
import type { HaTextField } from "../../components/ha-textfield";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HaInput } from "../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DialogBoxParams } from "./show-dialog-box";
|
||||
|
||||
@@ -28,7 +28,7 @@ class DialogBox extends LitElement {
|
||||
|
||||
@state() private _validInput = true;
|
||||
|
||||
@query("ha-textfield") private _textField?: HaTextField;
|
||||
@query("ha-input") private _textField?: HaInput;
|
||||
|
||||
private _closePromise?: Promise<void>;
|
||||
|
||||
@@ -109,14 +109,13 @@ class DialogBox extends LitElement {
|
||||
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
|
||||
${this._params.prompt
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
autofocus
|
||||
value=${ifDefined(this._params.defaultValue)}
|
||||
.placeholder=${this._params.placeholder}
|
||||
.label=${this._params.inputLabel
|
||||
? this._params.inputLabel
|
||||
: ""}
|
||||
.suffix=${this._params.inputSuffix}
|
||||
.type=${this._params.inputType
|
||||
? this._params.inputType
|
||||
: "text"}
|
||||
@@ -124,9 +123,13 @@ class DialogBox extends LitElement {
|
||||
.max=${this._params.inputMax}
|
||||
.disabled=${this._loading}
|
||||
@input=${this._validateInput}
|
||||
></ha-textfield>
|
||||
>
|
||||
${this._params.inputSuffix
|
||||
? html`<span slot="end">${this._params.inputSuffix}</span>`
|
||||
: nothing}
|
||||
</ha-input>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
${confirmPrompt
|
||||
@@ -240,7 +243,7 @@ class DialogBox extends LitElement {
|
||||
.secondary {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-textfield {
|
||||
ha-input {
|
||||
width: 100%;
|
||||
}
|
||||
.title.alert {
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface HassDialog<T = unknown> extends HTMLElement {
|
||||
}
|
||||
|
||||
export interface HassDialogNext<T = unknown> extends HTMLElement {
|
||||
dialogNext: true;
|
||||
params?: T;
|
||||
closeDialog?: (historyState?: any) => Promise<boolean> | boolean;
|
||||
}
|
||||
@@ -168,10 +169,12 @@ export const showDialog = async (
|
||||
dialogElement = await LOADED[dialogTag].element;
|
||||
}
|
||||
|
||||
if ("showDialog" in dialogElement!) {
|
||||
if ("dialogNext" in dialogElement! && dialogElement.dialogNext) {
|
||||
dialogElement!.params = dialogParams;
|
||||
} else if ("showDialog" in dialogElement!) {
|
||||
dialogElement.showDialog(dialogParams);
|
||||
} else {
|
||||
dialogElement!.params = dialogParams;
|
||||
throw new Error("Unknown dialog type loaded");
|
||||
}
|
||||
|
||||
(parentElement || element).shadowRoot!.appendChild(dialogElement!);
|
||||
|
||||
@@ -7,13 +7,13 @@ import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-control-button";
|
||||
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-select";
|
||||
import "../../../../components/ha-textfield";
|
||||
import "../../../../components/ha-dialog";
|
||||
import type { HaSelectSelectEvent } from "../../../../components/ha-select";
|
||||
import "../../../../components/input/ha-input";
|
||||
import { SirenEntityFeature } from "../../../../data/siren";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -95,27 +95,31 @@ class MoreInfoSirenAdvancedControls extends LitElement {
|
||||
: nothing}
|
||||
${supportsVolume
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
type="number"
|
||||
.label=${this.hass.localize("ui.components.siren.volume")}
|
||||
.suffix=${"%"}
|
||||
.value=${this._volume ? this._volume * 100 : undefined}
|
||||
.value=${this._volume ? `${this._volume * 100}` : undefined}
|
||||
@change=${this._handleVolumeChange}
|
||||
.min=${0}
|
||||
.max=${100}
|
||||
.step=${1}
|
||||
></ha-textfield>
|
||||
>
|
||||
<span slot="end">%</span>
|
||||
</ha-input>
|
||||
`
|
||||
: nothing}
|
||||
${supportsDuration
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
type="number"
|
||||
.label=${this.hass.localize("ui.components.siren.duration")}
|
||||
.value=${this._duration}
|
||||
suffix="s"
|
||||
.value=${this._duration !== undefined
|
||||
? this._duration.toString()
|
||||
: undefined}
|
||||
@change=${this._handleDurationChange}
|
||||
></ha-textfield>
|
||||
>
|
||||
<span slot="end">s</span>
|
||||
</ha-input>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@customElement("more-info-configurator")
|
||||
@@ -33,15 +33,15 @@ export class MoreInfoConfigurator extends LitElement {
|
||||
? html`<ha-alert alert-type="error">
|
||||
${this.stateObj.attributes.errors}
|
||||
</ha-alert>`
|
||||
: ""}
|
||||
: nothing}
|
||||
${this.stateObj.attributes.fields.map(
|
||||
(field) =>
|
||||
html`<ha-textfield
|
||||
html`<ha-input
|
||||
.label=${field.name}
|
||||
.name=${field.id}
|
||||
.type=${field.type}
|
||||
@change=${this._fieldChanged}
|
||||
></ha-textfield>`
|
||||
></ha-input>`
|
||||
)}
|
||||
${this.stateObj.attributes.submit_caption
|
||||
? html`<p class="submit">
|
||||
@@ -53,7 +53,7 @@ export class MoreInfoConfigurator extends LitElement {
|
||||
${this.stateObj.attributes.submit_caption}
|
||||
</ha-button>
|
||||
</p>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ class MoreInfoInputDatetime extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
--ha-input-padding-bottom: 0;
|
||||
}
|
||||
ha-date-input + ha-time-input {
|
||||
margin-left: var(--ha-space-1);
|
||||
|
||||
@@ -536,9 +536,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
: undefined;
|
||||
|
||||
const entityName = stateObj
|
||||
? computeEntityName(stateObj, this.hass.entities)
|
||||
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
|
||||
: this._entry
|
||||
? computeEntityEntryName(this._entry)
|
||||
? computeEntityEntryName(this._entry, this.hass.devices)
|
||||
: entityId;
|
||||
|
||||
const deviceName = context?.device
|
||||
|
||||
@@ -107,7 +107,11 @@ class MoreInfoContent extends LitElement {
|
||||
if (!stateObj) {
|
||||
return null;
|
||||
}
|
||||
const entityName = computeEntityName(stateObj, hass.entities);
|
||||
const entityName = computeEntityName(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices
|
||||
);
|
||||
const { area } = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
|
||||
@@ -216,9 +216,6 @@ export class CloudStepSignin extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,13 +4,14 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import {
|
||||
computeDeviceName,
|
||||
computeDeviceNameDisplay,
|
||||
} from "../../common/entity/compute_device_name";
|
||||
import "../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import "../../components/ha-tts-voice-picker";
|
||||
import "../../components/input/ha-input";
|
||||
import type { AssistPipeline } from "../../data/assist_pipeline";
|
||||
import {
|
||||
listAssistPipelines,
|
||||
@@ -99,14 +100,14 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
: nothing}
|
||||
<div class="rows">
|
||||
<div class="row">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.integrations.config_flow.device_name"
|
||||
)}
|
||||
.placeholder=${computeDeviceNameDisplay(device, this.hass)}
|
||||
.value=${this._deviceName ?? computeDeviceName(device)}
|
||||
@change=${this._deviceNameChanged}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
</div>
|
||||
${this.assistConfiguration &&
|
||||
this.assistConfiguration.available_wake_words.length > 1
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
mdiUnfoldLessHorizontal,
|
||||
mdiUnfoldMoreHorizontal,
|
||||
} from "@mdi/js";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
@@ -29,14 +29,15 @@ import type {
|
||||
} from "../components/data-table/ha-data-table";
|
||||
import { showDataTableSettingsDialog } from "../components/data-table/show-dialog-data-table-settings";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-dialog-footer";
|
||||
import "../components/ha-dialog";
|
||||
import "../components/ha-dialog-footer";
|
||||
import "../components/ha-dropdown";
|
||||
import "../components/ha-icon-button";
|
||||
import "../components/ha-svg-icon";
|
||||
import type { HaDropdownSelectEvent } from "../components/ha-dropdown";
|
||||
import "../components/ha-dropdown-item";
|
||||
import "../components/search-input-outlined";
|
||||
import "../components/ha-icon-button";
|
||||
import "../components/ha-svg-icon";
|
||||
import "../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../components/input/ha-input-search";
|
||||
import { KeyboardShortcutMixin } from "../mixins/keyboard-shortcut-mixin";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import "./hass-tabs-subpage";
|
||||
@@ -193,7 +194,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
|
||||
@query("ha-data-table", true) private _dataTable!: HaDataTable;
|
||||
|
||||
@query("search-input-outlined") private _searchInput!: HTMLElement;
|
||||
@query("ha-input-search") private _searchInput!: HaInputSearch;
|
||||
|
||||
protected supportedShortcuts(): SupportedShortcuts {
|
||||
return {
|
||||
@@ -266,14 +267,13 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
</ha-assist-chip>`
|
||||
: nothing;
|
||||
|
||||
const searchBar = html`<search-input-outlined
|
||||
.hass=${this.hass}
|
||||
.filter=${this.filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
.label=${this.searchLabel}
|
||||
const searchBar = html`<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this.filter}
|
||||
@input=${this._handleSearchChange}
|
||||
.placeholder=${this.searchLabel}
|
||||
>
|
||||
</search-input-outlined>`;
|
||||
</ha-input-search>`;
|
||||
|
||||
const sortByMenu = Object.values(this.columns).find((col) => col.sortable)
|
||||
? html`
|
||||
@@ -715,11 +715,12 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
this._dataTable.clearSelection();
|
||||
};
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
if (this.filter === ev.detail.value) {
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
const target = ev.target as HaInputSearch;
|
||||
if (this.filter === target.value) {
|
||||
return;
|
||||
}
|
||||
this.filter = ev.detail.value;
|
||||
this.filter = target.value ?? "";
|
||||
fireEvent(this, "search-changed", { value: this.filter });
|
||||
}
|
||||
|
||||
@@ -795,7 +796,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
search-input-outlined {
|
||||
ha-input-search {
|
||||
flex: 1;
|
||||
}
|
||||
.search-toolbar {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import "../components/ha-button";
|
||||
import "../components/ha-icon-button";
|
||||
import "../components/ha-toast";
|
||||
import type { ToastClosedEventDetail } from "../components/ha-toast";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface ShowToastParams {
|
||||
@@ -39,7 +41,7 @@ class NotificationManager extends LitElement {
|
||||
await this._toast?.hide();
|
||||
}
|
||||
|
||||
if (!parameters || parameters.duration === 0) {
|
||||
if (parameters.duration === 0) {
|
||||
this._parameters = undefined;
|
||||
return;
|
||||
}
|
||||
@@ -57,7 +59,7 @@ class NotificationManager extends LitElement {
|
||||
this._toast?.show();
|
||||
}
|
||||
|
||||
private _toastClosed(_ev: HTMLElementEventMap["toast-closed"]) {
|
||||
private _toastClosed(_ev: HASSDomEvent<ToastClosedEventDetail>) {
|
||||
this._parameters = undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -202,10 +202,6 @@ class OnboardingCoreConfig extends LitElement {
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
}
|
||||
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.flex {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,8 @@ import "../components/ha-list";
|
||||
import "../components/ha-list-item";
|
||||
import "../components/ha-radio";
|
||||
import "../components/ha-spinner";
|
||||
import "../components/ha-textfield";
|
||||
import type { HaTextField } from "../components/ha-textfield";
|
||||
import "../components/input/ha-input";
|
||||
import type { HaInput } from "../components/input/ha-input";
|
||||
import "../components/map/ha-locations-editor";
|
||||
import type {
|
||||
HaLocationsEditor,
|
||||
@@ -103,22 +103,20 @@ class OnboardingLocation extends LitElement {
|
||||
</p>
|
||||
|
||||
<div class="location-search">
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
label=${this.onboardingLocalize(
|
||||
"ui.panel.page-onboarding.core-config.address_label"
|
||||
)}
|
||||
.disabled=${this._working}
|
||||
icon
|
||||
iconTrailing
|
||||
@keyup=${this._addressSearch}
|
||||
>
|
||||
<ha-svg-icon slot="leadingIcon" .path=${mdiMagnify}></ha-svg-icon>
|
||||
<ha-svg-icon slot="start" .path=${mdiMagnify}></ha-svg-icon>
|
||||
${this._working
|
||||
? html` <ha-spinner slot="trailingIcon" size="small"></ha-spinner> `
|
||||
? html`<ha-spinner slot="end" size="small"></ha-spinner>`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
@click=${this._handleButtonClick}
|
||||
slot="trailingIcon"
|
||||
slot="end"
|
||||
.disabled=${this._working}
|
||||
.label=${this.onboardingLocalize(
|
||||
this._search
|
||||
@@ -128,7 +126,7 @@ class OnboardingLocation extends LitElement {
|
||||
.path=${this._search ? mdiMapSearchOutline : mdiCrosshairsGps}
|
||||
></ha-icon-button>
|
||||
`}
|
||||
</ha-textfield>
|
||||
</ha-input>
|
||||
${this._places !== undefined
|
||||
? html`
|
||||
<ha-list activatable>
|
||||
@@ -204,10 +202,7 @@ class OnboardingLocation extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
setTimeout(
|
||||
() => this.renderRoot.querySelector("ha-textfield")!.focus(),
|
||||
100
|
||||
);
|
||||
setTimeout(() => this.renderRoot.querySelector("ha-input")!.focus(), 100);
|
||||
this.addEventListener("keyup", (ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
this._save(ev);
|
||||
@@ -299,11 +294,11 @@ class OnboardingLocation extends LitElement {
|
||||
|
||||
private async _addressSearch(ev: KeyboardEvent) {
|
||||
ev.stopPropagation();
|
||||
this._search = (ev.currentTarget as HaTextField).value.length > 0;
|
||||
this._search = ((ev.currentTarget as HaInput).value ?? "").length > 0;
|
||||
if (ev.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
this._searchAddress((ev.currentTarget as HaTextField).value);
|
||||
this._searchAddress((ev.currentTarget as HaInput).value ?? "");
|
||||
}
|
||||
|
||||
private async _searchAddress(address: string) {
|
||||
@@ -477,28 +472,6 @@ class OnboardingLocation extends LitElement {
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
ha-textfield > ha-icon-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
inset-inline-end: 10px;
|
||||
inset-inline-start: initial;
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--secondary-text-color);
|
||||
inset-inline-start: initial;
|
||||
inset-inline-end: 10px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-textfield > ha-spinner {
|
||||
position: relative;
|
||||
left: 12px;
|
||||
inset-inline-start: 12px;
|
||||
inset-inline-end: initial;
|
||||
}
|
||||
ha-locations-editor {
|
||||
display: block;
|
||||
height: 300px;
|
||||
@@ -519,7 +492,7 @@ class OnboardingLocation extends LitElement {
|
||||
height: 72px;
|
||||
}
|
||||
.attribution {
|
||||
/* textfield helper style */
|
||||
/* input helper style */
|
||||
margin: 0;
|
||||
padding: 4px 16px 12px 16px;
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
|
||||
@@ -141,9 +141,6 @@ class ConfirmEventDialogBox extends LitElement {
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
ha-textfield {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -271,6 +271,7 @@ class DialogCalendarEventDetail extends LitElement {
|
||||
color: var(--secondary-text-color);
|
||||
max-width: 300px;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-line;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -10,6 +10,11 @@ import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
formatDate,
|
||||
formatTime,
|
||||
parseDate,
|
||||
} from "../../common/datetime/calc_date";
|
||||
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
@@ -19,13 +24,13 @@ import "../../components/entity/ha-entity-picker";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-date-input";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-dialog-footer";
|
||||
import "../../components/ha-formfield";
|
||||
import "../../components/ha-switch";
|
||||
import "../../components/ha-textarea";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/ha-time-input";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/input/ha-input";
|
||||
import type { CalendarEventMutableParams } from "../../data/calendar";
|
||||
import {
|
||||
CalendarEntityFeature,
|
||||
@@ -40,11 +45,6 @@ import "../lovelace/components/hui-generic-entity-row";
|
||||
import "./ha-recurrence-rule-editor";
|
||||
import { showConfirmEventDialog } from "./show-confirm-event-dialog-box";
|
||||
import type { CalendarEventEditDialogParams } from "./show-dialog-calendar-event-editor";
|
||||
import {
|
||||
formatDate,
|
||||
formatTime,
|
||||
parseDate,
|
||||
} from "../../common/datetime/calc_date";
|
||||
|
||||
const CALENDAR_DOMAINS = ["calendar"];
|
||||
|
||||
@@ -170,7 +170,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
>`
|
||||
: ""}
|
||||
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
class="summary"
|
||||
name="summary"
|
||||
.label=${this.hass.localize("ui.components.calendar.event.summary")}
|
||||
@@ -179,8 +179,8 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
@input=${this._handleSummaryChanged}
|
||||
.validationMessage=${this.hass.localize("ui.common.error_required")}
|
||||
autofocus
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
></ha-input>
|
||||
<ha-input
|
||||
class="location"
|
||||
name="location"
|
||||
.label=${this.hass.localize(
|
||||
@@ -188,7 +188,7 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
)}
|
||||
.value=${this._location}
|
||||
@change=${this._handleLocationChanged}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
<ha-textarea
|
||||
class="description"
|
||||
name="description"
|
||||
@@ -624,7 +624,6 @@ class DialogCalendarEventEditor extends LitElement {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-textfield,
|
||||
ha-textarea {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import "../../components/chips/ha-filter-chip";
|
||||
import "../../components/ha-date-input";
|
||||
import "../../components/ha-select";
|
||||
import type { HaSelectSelectEvent } from "../../components/ha-select";
|
||||
import "../../components/ha-textfield";
|
||||
import "../../components/input/ha-input";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type {
|
||||
MonthlyRepeatItem,
|
||||
@@ -231,7 +231,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
|
||||
renderInterval() {
|
||||
return html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
id="interval"
|
||||
label=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.interval.label"
|
||||
@@ -239,12 +239,15 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
type="number"
|
||||
min="1"
|
||||
.value=${this._interval}
|
||||
.suffix=${this.hass.localize(
|
||||
`ui.components.calendar.event.repeat.interval.${this
|
||||
._freq!}` as LocalizeKeys
|
||||
)}
|
||||
@change=${this._onIntervalChange}
|
||||
></ha-textfield>
|
||||
>
|
||||
<span slot="end">
|
||||
${this.hass.localize(
|
||||
`ui.components.calendar.event.repeat.interval.${this
|
||||
._freq!}` as LocalizeKeys
|
||||
)}
|
||||
</span>
|
||||
</ha-input>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -267,7 +270,7 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
</ha-select>
|
||||
${this._end === "after"
|
||||
? html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
id="after"
|
||||
label=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.end_after.label"
|
||||
@@ -275,11 +278,14 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
type="number"
|
||||
min="1"
|
||||
.value=${this._count!}
|
||||
suffix=${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.end_after.ocurrences"
|
||||
)}
|
||||
@change=${this._onCountChange}
|
||||
></ha-textfield>
|
||||
>
|
||||
<span slot="end">
|
||||
${this.hass.localize(
|
||||
"ui.components.calendar.event.repeat.end_after.ocurrences"
|
||||
)}
|
||||
</span>
|
||||
</ha-input>
|
||||
`
|
||||
: nothing}
|
||||
${this._end === "on"
|
||||
@@ -452,15 +458,16 @@ export class RecurrenceRuleEditor extends LitElement {
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-textfield,
|
||||
ha-input,
|
||||
ha-select {
|
||||
display: block;
|
||||
margin-bottom: 16px;
|
||||
--ha-input-padding-bottom: 0;
|
||||
}
|
||||
.weekdays {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
ha-textfield:last-child,
|
||||
ha-input:last-child,
|
||||
ha-select:last-child,
|
||||
.weekdays:last-child {
|
||||
margin-bottom: 0;
|
||||
|
||||
@@ -65,6 +65,9 @@ const processAreasForClimate = (
|
||||
if (temperatureEntityId && hass.states[temperatureEntityId]) {
|
||||
areaCards.push({
|
||||
...computeTileCard(temperatureEntityId),
|
||||
name:
|
||||
hass.localize("component.sensor.entity_component.temperature.name") ||
|
||||
"Temperature",
|
||||
features: [{ type: "trend-graph" }],
|
||||
});
|
||||
}
|
||||
@@ -73,6 +76,9 @@ const processAreasForClimate = (
|
||||
if (humidityEntityId && hass.states[humidityEntityId]) {
|
||||
areaCards.push({
|
||||
...computeTileCard(humidityEntityId),
|
||||
name:
|
||||
hass.localize("component.sensor.entity_component.humidity.name") ||
|
||||
"Humidity",
|
||||
features: [{ type: "trend-graph" }],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@ import {
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { extractSearchParam } from "../../../../../common/url/search-params";
|
||||
import "../../../../../components/ha-spinner";
|
||||
import "../../../../../components/input/ha-input-search";
|
||||
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
import "../../../logs/error-log-card";
|
||||
import "../../../../../components/search-input";
|
||||
import { extractSearchParam } from "../../../../../common/url/search-params";
|
||||
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
|
||||
|
||||
@customElement("supervisor-app-log-tab")
|
||||
class SupervisorAppLogDashboard extends LitElement {
|
||||
@@ -29,12 +29,12 @@ class SupervisorAppLogDashboard extends LitElement {
|
||||
}
|
||||
return html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
@input=${this._filterChanged}
|
||||
.value=${this._filter}
|
||||
.placeholder=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
<div class="content">
|
||||
<error-log-card
|
||||
@@ -48,8 +48,8 @@ class SupervisorAppLogDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _filterChanged(ev) {
|
||||
this._filter = ev.detail.value;
|
||||
private async _filterChanged(ev: InputEvent) {
|
||||
this._filter = (ev.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -66,10 +66,10 @@ class SupervisorAppLogDashboard extends LitElement {
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
ha-input-search {
|
||||
padding: var(--ha-space-3) var(--ha-space-2);
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
@media all and (max-width: 870px) {
|
||||
:host {
|
||||
|
||||
@@ -9,7 +9,7 @@ import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/search-input";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type {
|
||||
HassioAddonRepository,
|
||||
HassioAddonsInfo,
|
||||
@@ -143,11 +143,11 @@ export class HaConfigAppsAvailable extends LitElement {
|
||||
? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
|
||||
: html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._filterChanged}
|
||||
></search-input>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._filterChanged}
|
||||
></ha-input-search>
|
||||
</div>
|
||||
|
||||
${repos}
|
||||
@@ -260,8 +260,8 @@ export class HaConfigAppsAvailable extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private _filterChanged(e) {
|
||||
this._filter = e.detail.value;
|
||||
private _filterChanged(e: InputEvent) {
|
||||
this._filter = (e.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -278,10 +278,10 @@ export class HaConfigAppsAvailable extends LitElement {
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
ha-input-search {
|
||||
padding: var(--ha-space-3) var(--ha-space-2);
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import "../../../components/ha-card";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/search-input";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type {
|
||||
HassioAddonInfo,
|
||||
HassioAddonsInfo,
|
||||
@@ -88,13 +88,12 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<div class="search">
|
||||
<search-input
|
||||
.hass=${this.hass}
|
||||
suffix
|
||||
.filter=${this._filter}
|
||||
@value-changed=${this._handleSearchChange}
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._filter}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</search-input>
|
||||
</ha-input-search>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card-group">
|
||||
@@ -190,8 +189,8 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
private _handleSearchChange(ev: InputEvent) {
|
||||
this._filter = (ev.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
@@ -245,10 +244,10 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
search-input {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
ha-input-search {
|
||||
padding: var(--ha-space-3) var(--ha-space-2);
|
||||
background: var(--sidebar-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-aliases-editor";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-floor-picker";
|
||||
@@ -18,8 +19,8 @@ import type { HaPictureUpload } from "../../../components/ha-picture-upload";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-suggest-with-ai-button";
|
||||
import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../components/input/ha-input";
|
||||
import type { GenDataTaskResult } from "../../../data/ai_task";
|
||||
import type {
|
||||
AreaRegistryEntry,
|
||||
@@ -30,9 +31,9 @@ import {
|
||||
SENSOR_DEVICE_CLASS_HUMIDITY,
|
||||
SENSOR_DEVICE_CLASS_TEMPERATURE,
|
||||
} from "../../../data/sensor";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
|
||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import {
|
||||
@@ -144,7 +145,7 @@ class DialogAreaDetail
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
autofocus
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
@@ -152,8 +153,9 @@ class DialogAreaDetail
|
||||
.validationMessage=${this.hass.localize(
|
||||
"ui.panel.config.areas.editor.name_required"
|
||||
)}
|
||||
auto-validate
|
||||
required
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
@@ -416,9 +418,9 @@ class DialogAreaDetail
|
||||
return deviceReg && deviceReg.area_id === areaId;
|
||||
};
|
||||
|
||||
private _nameChanged(ev) {
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._name = ev.target.value;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
}
|
||||
|
||||
private _floorChanged(ev) {
|
||||
@@ -509,9 +511,6 @@ class DialogAreaDetail
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
--expansion-panel-content-padding: 0;
|
||||
}
|
||||
|
||||
@@ -11,13 +11,14 @@ import "../../../components/ha-alert";
|
||||
import "../../../components/ha-aliases-editor";
|
||||
import "../../../components/ha-area-picker";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon-picker";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-dialog";
|
||||
import "../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../components/input/ha-input";
|
||||
import { updateAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type {
|
||||
FloorRegistryEntry,
|
||||
@@ -133,7 +134,7 @@ class DialogFloorDetail extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
autofocus
|
||||
.value=${this._name}
|
||||
@input=${this._nameChanged}
|
||||
@@ -142,20 +143,20 @@ class DialogFloorDetail extends LitElement {
|
||||
"ui.panel.config.floors.editor.name_required"
|
||||
)}
|
||||
required
|
||||
></ha-textfield>
|
||||
auto-validate
|
||||
></ha-input>
|
||||
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.value=${this._level}
|
||||
@input=${this._levelChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.floors.editor.level"
|
||||
)}
|
||||
type="number"
|
||||
.helper=${this.hass.localize(
|
||||
.hint=${this.hass.localize(
|
||||
"ui.panel.config.floors.editor.level_helper"
|
||||
)}
|
||||
helperPersistent
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
|
||||
<ha-icon-picker
|
||||
.hass=${this.hass}
|
||||
@@ -293,14 +294,17 @@ class DialogFloorDetail extends LitElement {
|
||||
this._addedAreas = new Set(this._addedAreas);
|
||||
}
|
||||
|
||||
private _nameChanged(ev) {
|
||||
private _nameChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._name = ev.target.value;
|
||||
this._name = (ev.target as HaInput).value ?? "";
|
||||
}
|
||||
|
||||
private _levelChanged(ev) {
|
||||
private _levelChanged(ev: InputEvent) {
|
||||
this._error = undefined;
|
||||
this._level = ev.target.value === "" ? null : Number(ev.target.value);
|
||||
this._level =
|
||||
(ev.target as HaInput).value === ""
|
||||
? null
|
||||
: Number((ev.target as HaInput).value);
|
||||
}
|
||||
|
||||
private _iconChanged(ev) {
|
||||
@@ -355,9 +359,8 @@ class DialogFloorDetail extends LitElement {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-textfield {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
ha-input {
|
||||
--ha-input-padding-bottom: var(--ha-space-4);
|
||||
}
|
||||
ha-floor-icon {
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -95,6 +95,8 @@ import "./types/ha-automation-action-set_conversation_response";
|
||||
import "./types/ha-automation-action-stop";
|
||||
import "./types/ha-automation-action-wait_for_trigger";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../../common/entity/compute_object_id";
|
||||
|
||||
export const getAutomationActionType = memoizeOne(
|
||||
(action: Action | undefined) => {
|
||||
@@ -185,6 +187,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
@state() private _collapsed = true;
|
||||
|
||||
@state() private _isNew = false;
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@query("ha-automation-action-editor")
|
||||
@@ -237,12 +241,20 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
private _renderRow() {
|
||||
const type = getAutomationActionType(this.action);
|
||||
|
||||
const target =
|
||||
type === "service" && "target" in this.action
|
||||
? (this.action as ServiceAction).target
|
||||
: type === "device_id" && (this.action as DeviceAction).device_id
|
||||
? { device_id: (this.action as DeviceAction).device_id }
|
||||
: undefined;
|
||||
const action = type === "service" && (this.action as ServiceAction).action;
|
||||
|
||||
const actionHasTarget =
|
||||
action &&
|
||||
"target" in
|
||||
(this.hass.services?.[computeDomain(action)]?.[
|
||||
computeObjectId(action)
|
||||
] || {});
|
||||
|
||||
const target = actionHasTarget
|
||||
? (this.action as ServiceAction).target
|
||||
: type === "device_id" && (this.action as DeviceAction).device_id
|
||||
? { device_id: (this.action as DeviceAction).device_id }
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
${type === "service" && "action" in this.action && this.action.action
|
||||
@@ -265,7 +277,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
${capitalizeFirstLetter(
|
||||
describeAction(this.hass, this._entityReg, this.action)
|
||||
)}
|
||||
${target ? this._renderTargets(target) : nothing}
|
||||
${target !== undefined || (actionHasTarget && !this._isNew)
|
||||
? this._renderTargets(target, actionHasTarget && !this._isNew)
|
||||
: nothing}
|
||||
${type !== "condition" &&
|
||||
(this.action as NonConditionAction).continue_on_error === true
|
||||
? html`<ha-svg-icon
|
||||
@@ -545,7 +559,10 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
>${this._renderRow()}</ha-automation-row
|
||||
>`
|
||||
: html`
|
||||
<ha-expansion-panel left-chevron>
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
@expanded-changed=${this._expansionPanelChanged}
|
||||
>
|
||||
${this._renderRow()}
|
||||
</ha-expansion-panel>
|
||||
`}
|
||||
@@ -575,10 +592,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(target?: HassServiceTarget) =>
|
||||
(target?: HassServiceTarget, targetRequired = false) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
@@ -802,6 +820,12 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _expansionPanelChanged(ev: CustomEvent) {
|
||||
if (!ev.detail.expanded) {
|
||||
this._isNew = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleSidebar(ev: Event) {
|
||||
ev?.stopPropagation();
|
||||
|
||||
@@ -812,6 +836,10 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
this.openSidebar();
|
||||
}
|
||||
|
||||
public markAsNew(): void {
|
||||
this._isNew = true;
|
||||
}
|
||||
|
||||
public openSidebar(action?: Action): void {
|
||||
const sidebarAction = action ?? this.action;
|
||||
const actionType = getAutomationActionType(sidebarAction);
|
||||
@@ -822,6 +850,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
},
|
||||
close: (focus?: boolean) => {
|
||||
this._selected = false;
|
||||
this._isNew = false;
|
||||
fireEvent(this, "close-sidebar");
|
||||
if (focus) {
|
||||
this.focus();
|
||||
|
||||
@@ -161,6 +161,7 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
|
||||
|
||||
if (mode === "new") {
|
||||
row.expand();
|
||||
row.markAsNew();
|
||||
}
|
||||
|
||||
if (!this.optionsInSidebar) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/entity/ha-entity-picker";
|
||||
import "../../../../../components/ha-service-picker";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import "../../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../../components/ha-yaml-editor";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import type { EventAction } from "../../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
@@ -44,14 +45,14 @@ export class HaEventAction extends LitElement implements ActionElement {
|
||||
const { event, event_data } = this.action;
|
||||
|
||||
return html`
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.event.event"
|
||||
)}
|
||||
.value=${event}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._eventChanged}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
@@ -74,18 +75,12 @@ export class HaEventAction extends LitElement implements ActionElement {
|
||||
handleChangeEvent(this, ev);
|
||||
}
|
||||
|
||||
private _eventChanged(ev: CustomEvent): void {
|
||||
private _eventChanged(ev: InputEvent): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, event: (ev.target as any).value },
|
||||
value: { ...this.action, event: (ev.target as HaInput).value },
|
||||
});
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-textfield {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, queryAll } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import type { Action, IfAction } from "../../../../../data/script";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-textfield";
|
||||
import type { RepeatAction } from "../../../../../data/script";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -172,14 +171,7 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
ha-textfield {
|
||||
margin-top: 16px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return [haStyle];
|
||||
}
|
||||
|
||||
private _getSelectorElements() {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { assert } from "superstruct";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { hasTemplate } from "../../../../../common/string/has-template";
|
||||
import "../../../../../components/ha-service-control";
|
||||
import "../../../../../components/input/ha-input";
|
||||
import type { HaInput } from "../../../../../components/input/ha-input";
|
||||
import type { ServiceAction } from "../../../../../data/script";
|
||||
import { serviceActionStruct } from "../../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -105,7 +107,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
"ui.panel.config.automation.editor.actions.type.service.has_response"
|
||||
)}
|
||||
</span>
|
||||
<ha-textfield
|
||||
<ha-input
|
||||
.value=${this._action.response_variable || ""}
|
||||
.required=${!this.hass.services[domain][service].response!
|
||||
.optional}
|
||||
@@ -114,7 +116,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
!this._action.response_variable &&
|
||||
!this._responseChecked)}
|
||||
@change=${this._responseVariableChanged}
|
||||
></ha-textfield>
|
||||
></ha-input>
|
||||
</ha-settings-row>`
|
||||
: nothing}
|
||||
`;
|
||||
@@ -143,9 +145,12 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
}
|
||||
|
||||
private _responseVariableChanged(ev) {
|
||||
const value = { ...this.action, response_variable: ev.target.value };
|
||||
if (!ev.target.value) {
|
||||
private _responseVariableChanged(ev: InputEvent) {
|
||||
const value = {
|
||||
...this.action,
|
||||
response_variable: (ev.target as HaInput).value,
|
||||
};
|
||||
if (!(ev.target as HaInput).value) {
|
||||
delete value.response_variable;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value });
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user