Compare commits

..

15 Commits

Author SHA1 Message Date
pcan08 0dfb801ff6 Devices page: forget filter from url (#51986)
* fix(devices): clear URL-injected filters on leaving devices dashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(devices): restore previous filters after URL-injected navigation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(devices): use separate storage and display filters

Replace the disconnect-callback approach with two distinct filter states:
- _storageFilters: persisted to sessionStorage, updated only when not in
  URL mode (manual filter changes and clear)
- _filters: display-only state, initialized from _storageFilters on first
  render, overwritten by URL params without touching storage

_storageFilters is frozen while _fromUrl is true, preserving the user's
previous manual filters for the next normal visit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 08:45:51 +02:00
renovate[bot] d94dcf50fb Update dependency eslint-plugin-lit to v2.3.1 (#52057)
* Update dependency eslint-plugin-lit to v2.3.1

* Fix lit/prefer-query-decorators violations

eslint-plugin-lit 2.3.0 introduced this rule. Replace querySelector
calls with @query/@queryAll decorators where the selector is static.
Use per-line disables for dynamic selectors that can't use decorators.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-05-15 09:37:14 +03:00
Petar Petrov fb1f5ef722 Dev tools -> Templates: observe tip height with ResizeObserver (#52048)
Replaces the per-render scrollHeight read from #52012 with a
ResizeObserver started in firstUpdated, writing --tip-height directly to
the host style. Removes the need for a manual refresh when the viewport
crosses a wrap threshold, drops the unreachable isNaN check, and lets
the @query decorator stand in for the editor-tip id.
2026-05-15 08:37:04 +02:00
pcan08 e5d5797d91 Add mute to media player volume slider feature (#52050)
* feat: add mute button to media-player-volume-slider card feature

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(tile-card): extract mute button logic into shared utility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 09:31:45 +03:00
pcan08 adee24f745 fix filter badge count (increment) on panel re-open (#52054)
fix(filter): prevent badge count from incrementing on panel re-open

Integrations and domains filter panels use lazy rendering: the list is
destroyed on close and recreated on open. On recreation, MWC fires a
`selected` event with a diff for each pre-selected item, which the
diff-based handler interpreted as a new user selection, appending
duplicates to `this.value` on every expansion.

Switch both handlers to the full-set approach (`SelectedDetail<Set<number>>`)
already used by labels, states, and voice-assistants, rebuilding the value
from the complete index set. Add the `preserved` pattern to retain
selections hidden by the search filter. Also add `_value` to the `_domains`
memoize signature to ensure cache invalidation when the selection changes.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 08:38:13 +03:00
karwosts 1b695e24d0 Ensure statistics-graph-card uses correct external stat names (#52055) 2026-05-15 08:36:33 +03:00
pcan08 7f9259edf9 Add shuffle and repeat controls to media-player-playback feature (#52052)
feat(tile-card): add shuffle and repeat controls to media-player-playback card feature

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 20:55:14 +02:00
renovate[bot] 6954dc1a54 Update dependency typescript-eslint to v8.59.3 (#52056)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-14 18:37:14 +00:00
renovate[bot] 032d0fb332 Update vitest monorepo to v4.1.6 (#52053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-14 20:28:13 +02:00
Jan-Philipp Benecke 43ed97da43 Migrate gallery drawer to ha-drawer and drop mwc-drawer dependency (#52031)
* Migrate gallery drawer to ha-drawer and drop md-drawer dependency

* Trigger Build

* Fix scrolling
2026-05-14 15:52:24 +02:00
Aidan Timson 9f4d35bc05 Remove advanced mode navigation gating (#52045) 2026-05-14 15:37:28 +03:00
ildar170975 11afde6b5f Dev tools -> Templates: fix editor height (#52012)
* fix editor height

* get a height of ha-tip by `Element.scrollHeight`

* minor cleanup
2026-05-14 15:34:23 +03:00
pcan08 1b0dcb33b1 Add source filtering to media-player-source card feature (#52046)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Aidan Timson <aidan@timmo.dev>
2026-05-14 12:26:36 +00:00
Aidan Timson 67eecbc51d Remove "advanced" service controls (#52041)
Remove "Advanced" service controls
2026-05-14 15:19:52 +03:00
renovate[bot] 969ccf85d2 Update dependency @rsdoctor/rspack-plugin to v1.5.11 (#52040) 2026-05-14 08:51:19 +00:00
103 changed files with 1096 additions and 907 deletions
-1
View File
@@ -1,4 +1,3 @@
import "@material/mwc-drawer";
import "@material/mwc-top-app-bar-fixed";
import { html, css, LitElement } from "lit";
import { customElement } from "lit/decorators";
+25 -10
View File
@@ -1,4 +1,3 @@
import "@material/mwc-drawer";
import "@material/mwc-top-app-bar-fixed";
import { mdiMenu, mdiSwapHorizontal } from "@mdi/js";
import type { PropertyValues } from "lit";
@@ -7,6 +6,8 @@ import { customElement, query, state } from "lit/decorators";
import { dynamicElement } from "../../src/common/dom/dynamic-element-directive";
import { setDirectionStyles } from "../../src/common/util/compute_rtl";
import "../../src/components/ha-button";
import "../../src/components/ha-drawer";
import type { HaDrawer } from "../../src/components/ha-drawer";
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
import "../../src/components/ha-icon-button";
import "../../src/components/ha-svg-icon";
@@ -39,8 +40,8 @@ class HaGallery extends LitElement {
@query("notification-manager")
private _notifications!: HTMLElementTagNameMap["notification-manager"];
@query("mwc-drawer")
private _drawer!: HTMLElementTagNameMap["mwc-drawer"];
@query("ha-drawer")
private _drawer!: HaDrawer;
private _narrow = window.matchMedia("(max-width: 600px)").matches;
@@ -75,15 +76,14 @@ class HaGallery extends LitElement {
}
return html`
<mwc-drawer
hasHeader
<ha-drawer
.direction=${this._rtl ? "rtl" : "ltr"}
.open=${!this._narrow}
.type=${this._narrow ? "modal" : "dismissible"}
>
<span slot="title">Home Assistant Design</span>
<!-- <span slot="subtitle">subtitle</span> -->
<div class="drawer-title">Home Assistant Design</div>
<div class="sidebar">${sidebar}</div>
<div slot="appContent">
<div slot="appContent" class="app-content">
<mwc-top-app-bar-fixed>
<ha-icon-button
slot="navigationIcon"
@@ -144,7 +144,7 @@ class HaGallery extends LitElement {
</div>
</div>
</div>
</mwc-drawer>
</ha-drawer>
<notification-manager
.hass=${FAKE_HASS}
id="notifications"
@@ -226,12 +226,27 @@ class HaGallery extends LitElement {
-ms-user-select: initial;
-webkit-user-select: initial;
-moz-user-select: initial;
--ha-sidebar-width: 256px;
}
.sidebar {
box-sizing: border-box;
max-height: calc(100vh - 64px);
overflow-y: auto;
padding: 4px;
}
.drawer-title {
align-items: center;
box-sizing: border-box;
color: var(--primary-text-color);
display: flex;
font-size: var(--ha-font-size-l);
font-weight: var(--ha-font-weight-medium);
min-height: 64px;
padding: 0 16px;
}
.sidebar a {
color: var(--primary-text-color);
display: block;
@@ -255,7 +270,7 @@ class HaGallery extends LitElement {
opacity: 0.12;
}
div[slot="appContent"] {
.app-content {
display: flex;
flex-direction: column;
min-height: 100vh;
+5 -6
View File
@@ -64,7 +64,6 @@
"@lit/reactive-element": "2.1.2",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
"@material/mwc-drawer": "0.27.0",
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@material/mwc-top-app-bar": "0.27.0",
@@ -142,7 +141,7 @@
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.10",
"@rsdoctor/rspack-plugin": "1.5.11",
"@rspack/core": "2.0.2",
"@rspack/dev-server": "2.0.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
@@ -162,7 +161,7 @@
"@types/sortablejs": "1.15.9",
"@types/tar": "7.0.87",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.1.5",
"@vitest/coverage-v8": "4.1.6",
"babel-loader": "10.1.1",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.4",
@@ -171,7 +170,7 @@
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-plugin-import-x": "4.16.2",
"eslint-plugin-lit": "2.2.1",
"eslint-plugin-lit": "2.3.1",
"eslint-plugin-lit-a11y": "5.1.1",
"eslint-plugin-unused-imports": "4.4.1",
"eslint-plugin-wc": "3.1.0",
@@ -203,9 +202,9 @@
"terser-webpack-plugin": "5.6.0",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.3",
"typescript-eslint": "8.59.2",
"typescript-eslint": "8.59.3",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.5",
"vitest": "4.1.6",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.4.1#~/.yarn/patches/workbox-build-npm-7.4.1-c84561662c.patch"
+4 -3
View File
@@ -54,6 +54,8 @@ export class HaAuthFlow extends LitElement {
@query("ha-auth-form") private _form?: HaAuthForm;
@query("ha-form") private _haForm?: HTMLElement;
createRenderRoot() {
return this;
}
@@ -160,9 +162,8 @@ export class HaAuthFlow extends LitElement {
// 100ms to give all the form elements time to initialize.
setTimeout(() => {
const form = this.renderRoot.querySelector("ha-form");
if (form) {
(form as any).focus();
if (this._haForm) {
(this._haForm as any).focus();
}
}, 100);
}
-6
View File
@@ -5,7 +5,6 @@ import { isComponentLoaded } from "./is_component_loaded";
export const canShowPage = (hass: HomeAssistant, page: PageNavigation) =>
(isCore(page) || isLoadedIntegration(hass, page)) &&
!hideAdvancedPage(hass, page) &&
isNotLoadedIntegration(hass, page);
export const isLoadedIntegration = (
@@ -27,8 +26,3 @@ export const isNotLoadedIntegration = (
);
export const isCore = (page: PageNavigation) => page.core;
export const isAdvancedPage = (page: PageNavigation) => page.advancedOnly;
export const userWantsAdvanced = (hass: HomeAssistant) =>
hass.userData?.showAdvanced;
export const hideAdvancedPage = (hass: HomeAssistant, page: PageNavigation) =>
isAdvancedPage(page) && !userWantsAdvanced(hass);
+4 -3
View File
@@ -19,7 +19,7 @@ import type {
} from "echarts/types/dist/shared";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { ensureArray } from "../../common/array/ensure-array";
@@ -102,6 +102,8 @@ export class HaChartBase extends LitElement {
@state() private _hiddenDatasets = new Set<string>();
@query(".chart") private _chartContainer?: HTMLDivElement;
private _modifierPressed = false;
private _isTouchDevice = "ontouchstart" in window;
@@ -469,7 +471,6 @@ export class HaChartBase extends LitElement {
private async _setupChart() {
if (this._loading) return;
const container = this.renderRoot.querySelector(".chart") as HTMLDivElement;
this._loading = true;
try {
if (this.chart) {
@@ -484,7 +485,7 @@ export class HaChartBase extends LitElement {
const style = getComputedStyle(this);
echarts.registerTheme("custom", this._createTheme(style));
this.chart = echarts.init(container, "custom");
this.chart = echarts.init(this._chartContainer!, "custom");
this.chart.on("datazoom", (e: any) => {
this._handleDataZoomEvent(e);
});
@@ -12,7 +12,6 @@ import type { LineChartEntity, LineChartState } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
import type { ECOption } from "../../resources/echarts/echarts";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
@@ -118,7 +117,9 @@ export class StateHistoryChartLine extends LitElement {
private _chartTime: Date = new Date();
private _yAxisFractionDigits = 1;
private _previousYAxisLabelValue = 0;
private _yAxisMaximumFractionDigits = 0;
protected render() {
return html`
@@ -435,14 +436,6 @@ export class StateHistoryChartLine extends LitElement {
const datasets: LineSeriesOption[] = [];
const entityIds: string[] = [];
const datasetToDataIndex: number[] = [];
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number | null | undefined) => {
if (typeof v === "number" && Number.isFinite(v)) {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
}
};
if (entityStates.length === 0) {
return;
}
@@ -478,7 +471,6 @@ export class StateHistoryChartLine extends LitElement {
d.data!.push([timestamp, prevValues[i]]);
}
d.data!.push([timestamp, datavalues[i]]);
trackY(datavalues[i]);
});
prevValues = datavalues;
};
@@ -829,7 +821,6 @@ export class StateHistoryChartLine extends LitElement {
const currentValue = stateObj ? safeParseFloat(stateObj.state) : null;
if (currentValue !== null) {
data[0].data!.push([now, currentValue]);
trackY(currentValue);
}
}
@@ -837,7 +828,6 @@ export class StateHistoryChartLine extends LitElement {
Array.prototype.push.apply(datasets, data);
});
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._entityIds = entityIds;
this._datasetToDataIndex = datasetToDataIndex;
@@ -871,8 +861,20 @@ export class StateHistoryChartLine extends LitElement {
}
private _formatYAxisLabel = (value: number) => {
// show the first significant digit for tiny values
const maximumFractionDigits = Math.max(
1,
// use the difference to the previous value to determine the number of significant digits #25526
-Math.floor(
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
)
);
this._yAxisMaximumFractionDigits = Math.max(
this._yAxisMaximumFractionDigits,
maximumFractionDigits
);
const label = formatNumber(value, this.hass.locale, {
maximumFractionDigits: this._yAxisFractionDigits,
maximumFractionDigits: this._yAxisMaximumFractionDigits,
});
const width = measureTextWidth(label, 12) + 5;
if (width > this._yWidth) {
@@ -882,6 +884,7 @@ export class StateHistoryChartLine extends LitElement {
chartIndex: this.chartIndex,
});
}
this._previousYAxisLabelValue = value;
return label;
};
+14 -11
View File
@@ -2,7 +2,13 @@ import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiRestart } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, eventOptions, property, state } from "lit/decorators";
import {
customElement,
eventOptions,
property,
queryAll,
state,
} from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { restoreScroll } from "../../common/decorators/restore-scroll";
import type {
@@ -104,6 +110,11 @@ export class StateHistoryCharts extends LitElement {
@state() private _hasZoomedCharts = false;
@queryAll("state-history-chart-line, state-history-chart-timeline")
private _chartComponents!: NodeListOf<
StateHistoryChartLine | StateHistoryChartTimeline
>;
private _isSyncing = false;
// @ts-ignore
@@ -327,11 +338,7 @@ export class StateHistoryCharts extends LitElement {
this._isSyncing = true;
requestAnimationFrame(() => {
const chartComponents = this.renderRoot.querySelectorAll(
"state-history-chart-line, state-history-chart-timeline"
) as unknown as (StateHistoryChartLine | StateHistoryChartTimeline)[];
chartComponents.forEach((chartComponent, index) => {
this._chartComponents.forEach((chartComponent, index) => {
if (index === sourceChartIndex) {
return;
}
@@ -350,11 +357,7 @@ export class StateHistoryCharts extends LitElement {
this._isSyncing = true;
requestAnimationFrame(() => {
const chartComponents = this.renderRoot.querySelectorAll(
"state-history-chart-line, state-history-chart-timeline"
);
chartComponents.forEach((chartComponent: any) => {
this._chartComponents.forEach((chartComponent: any) => {
const chartBase =
chartComponent.renderRoot?.querySelector("ha-chart-base");
+17 -20
View File
@@ -41,7 +41,6 @@ import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import { fillDataGapsAndRoundCaps } from "./round-caps";
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean",
@@ -132,7 +131,7 @@ export class StatisticsChart extends LitElement {
private _computedStyle?: CSSStyleDeclaration;
private _yAxisFractionDigits = 1;
private _previousYAxisLabelValue = 0;
protected shouldUpdate(changedProps: PropertyValues<this>): boolean {
return changedProps.size > 1 || !changedProps.has("hass");
@@ -144,7 +143,8 @@ export class StatisticsChart extends LitElement {
changedProps.has("statTypes") ||
changedProps.has("chartType") ||
changedProps.has("hideLegend") ||
changedProps.has("_hiddenStats")
changedProps.has("_hiddenStats") ||
changedProps.has("names")
) {
this._generateData();
}
@@ -496,14 +496,6 @@ export class StatisticsChart extends LitElement {
const chartStacked = this.chartType.endsWith("stack");
const statisticsData = Object.entries(this.statisticsData);
const totalDataSets: typeof this._chartData = [];
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number | null | undefined) => {
if (typeof v === "number" && Number.isFinite(v)) {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
}
};
const legendData: {
id: string;
name: string;
@@ -609,9 +601,6 @@ export class StatisticsChart extends LitElement {
d.data!.push([prevEndTime, null]);
}
d.data!.push([start, ...dataValues[i]!]);
// For band-top rows dataValues[i] is [diff, top]; the actual Y is
// the last element. For regular rows it's [value]. Same call works.
trackY(dataValues[i][dataValues[i].length - 1]);
} else {
let time = start;
if (centerBars) {
@@ -622,7 +611,6 @@ export class StatisticsChart extends LitElement {
// Data value should always be a scalar for bar charts. Pass in
// real start time as extra value to allow formatting tooltip.
d.data!.push([time, dataValues[i][0]!, start, end]);
trackY(dataValues[i][0]);
}
});
prevValues = dataValues;
@@ -835,7 +823,6 @@ export class StatisticsChart extends LitElement {
val.push(currentValue);
}
statDataSets[i].data!.push([now, ...val]);
trackY(val[val.length - 1]);
});
}
}
@@ -869,7 +856,6 @@ export class StatisticsChart extends LitElement {
});
});
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = totalDataSets;
if (legendData.length !== this._legendData?.length) {
// only update the legend if it has changed or it will trigger options update
@@ -903,10 +889,21 @@ export class StatisticsChart extends LitElement {
return Math.abs(value) < 1 ? value : roundingFn(value);
}
private _formatYAxisLabel = (value: number) =>
formatNumber(value, this.hass.locale, {
maximumFractionDigits: this._yAxisFractionDigits,
private _formatYAxisLabel = (value: number) => {
// show the first significant digit for tiny values
const maximumFractionDigits = Math.max(
1,
// use the difference to the previous value to determine the number of significant digits #25526
-Math.floor(
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
)
);
const label = formatNumber(value, this.hass.locale, {
maximumFractionDigits,
});
this._previousYAxisLabelValue = value;
return label;
};
static styles = css`
:host {
@@ -1,9 +0,0 @@
// Derive the number of decimal digits to use for Y-axis labels from the
// observed data range. We estimate the tick interval as `range / 10` (twice
// ECharts' default splitNumber of 5, as a safety margin against finer "nice"
// intervals), then derive `ceil(-log10(interval))`.
export function computeYAxisFractionDigits(min: number, max: number): number {
const range = max - min;
if (!Number.isFinite(range) || range <= 0) return 1;
return Math.max(0, Math.ceil(-Math.log10(range / 10)));
}
@@ -24,6 +24,7 @@ import "../ha-icon-button";
import "../ha-icon-button-next";
import "../ha-icon-button-prev";
import "../ha-textarea";
import type { HaTextArea } from "../ha-textarea";
import "./date-range-picker";
export type DateRangePickerRanges = Record<string, [Date, Date]>;
@@ -98,6 +99,8 @@ export class HaDateRangePicker extends LitElement {
@query(".container") private _containerElement?: HTMLDivElement;
@query("ha-textarea") private _textareaElement?: HaTextArea;
private _narrow = false;
private _unsubscribeTinyKeys?: () => void;
@@ -335,9 +338,8 @@ export class HaDateRangePicker extends LitElement {
};
private _setTextareaFocusStyle(focused: boolean) {
const textarea = this.renderRoot.querySelector("ha-textarea");
if (textarea) {
textarea.setFocused(focused);
if (this._textareaElement) {
this._textareaElement.setFocused(focused);
}
}
+5 -3
View File
@@ -1,6 +1,6 @@
import "@home-assistant/webawesome/dist/components/popover/popover";
import { css, html, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../common/dom/fire_event";
import { ScrollLockMixin } from "../mixins/scroll-lock-mixin";
@@ -25,6 +25,8 @@ export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
@state() private _shouldRenderPopover = false;
@query("wa-popover") private _popoverElement?: HTMLElement;
protected willUpdate(changedProperties: PropertyValues<this>) {
if (
changedProperties.has("dialogAnchor") ||
@@ -188,7 +190,7 @@ export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
}
private _handlePopoverPointerDown(ev: PointerEvent) {
const popover = this.renderRoot.querySelector("wa-popover");
const popover = this._popoverElement;
const dialog = popover?.shadowRoot?.querySelector(
"dialog"
) as HTMLDialogElement | null;
@@ -215,7 +217,7 @@ export class HaAdaptivePopover extends ScrollLockMixin(HaAdaptiveDialog) {
}
private _pulsePopover() {
const popover = this.renderRoot.querySelector("wa-popover");
const popover = this._popoverElement;
const popup = popover?.shadowRoot?.querySelector("wa-popup") as {
popup?: HTMLElement;
} | null;
+4 -1
View File
@@ -9,6 +9,7 @@ import {
customElement,
property,
query,
queryAll,
state as litState,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
@@ -31,6 +32,8 @@ export class HaAnsiToHtml extends LitElement {
@query("pre") private _pre?: HTMLPreElement;
@queryAll("div") private _divs!: NodeListOf<HTMLDivElement>;
@litState() private _filter = "";
protected render(): TemplateResult {
@@ -320,7 +323,7 @@ export class HaAnsiToHtml extends LitElement {
*/
filterLines(filter: string): boolean {
this._filter = filter;
const lines = this.shadowRoot?.querySelectorAll("div") || [];
const lines = this._divs;
let numberOfFoundLines = 0;
if (!filter) {
lines.forEach((line) => {
+5 -4
View File
@@ -1,6 +1,6 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
@@ -24,11 +24,12 @@ class HaBluePrintPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
@query("ha-select") private _select?: HTMLElement;
public open() {
const select = this.shadowRoot?.querySelector("ha-select");
if (select) {
if (this._select) {
// @ts-expect-error
select.menuOpen = true;
this._select.menuOpen = true;
}
}
+5 -5
View File
@@ -75,6 +75,8 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
@query("#body") private _bodyElement!: HTMLDivElement;
@query("[autofocus]") private _autofocusElement?: HTMLElement;
protected get scrollableElement(): HTMLElement | null {
return this._bodyElement;
}
@@ -93,12 +95,12 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
await this.updateComplete;
requestAnimationFrame(() => {
const element = this._autofocusElement;
if (
this._hassConfig?.auth.external &&
isIosApp(this._hassConfig.auth.external)
) {
const element = this.renderRoot.querySelector("[autofocus]");
if (element !== null) {
if (element) {
if (!element.id) {
element.id = "ha-bottom-sheet-autofocus";
}
@@ -111,9 +113,7 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
}
return;
}
(
this.renderRoot.querySelector("[autofocus]") as HTMLElement | null
)?.focus();
element?.focus();
});
};
+1
View File
@@ -188,6 +188,7 @@ export class HaCodeEditor extends ReactiveElement {
this.codemirror.state,
[this._loadedCodeMirror.tags.comment]
);
// eslint-disable-next-line lit/prefer-query-decorators
return !!this.renderRoot.querySelector(`span.${className}`);
}
+1
View File
@@ -54,6 +54,7 @@ export class HaControlSelect extends LitElement {
this._activeIndex = index;
this.requestUpdate();
this.updateComplete.then(() => {
// eslint-disable-next-line lit/prefer-query-decorators
const option = this.shadowRoot?.querySelector(
`#option-${this.options![index].value}`
) as HTMLElement;
+27 -3
View File
@@ -25,7 +25,7 @@ export class HaDrawer extends LitElement {
@property({ reflect: true }) public direction: "ltr" | "rtl" = "ltr";
@property() public type = "";
@property({ reflect: true }) public type: "" | "dismissible" | "modal" = "";
@property({ type: Boolean, reflect: true }) public open = false;
@@ -105,7 +105,11 @@ export class HaDrawer extends LitElement {
}
private _handleDrawerTransitionStart = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || this._sidebarTransitionActive) {
if (
ev.propertyName !==
(this.type === "dismissible" ? "transform" : "width") ||
this._sidebarTransitionActive
) {
return;
}
this._sidebarTransitionActive = true;
@@ -116,7 +120,11 @@ export class HaDrawer extends LitElement {
};
private _handleDrawerTransitionEnd = (ev: TransitionEvent) => {
if (ev.propertyName !== "width" || !this._sidebarTransitionActive) {
if (
ev.propertyName !==
(this.type === "dismissible" ? "transform" : "width") ||
!this._sidebarTransitionActive
) {
return;
}
this._sidebarTransitionActive = false;
@@ -300,6 +308,22 @@ export class HaDrawer extends LitElement {
width var(--ha-animation-duration-normal) ease;
}
:host([type="dismissible"]) .sidebar-shell {
transition: transform var(--ha-animation-duration-normal) ease;
}
:host([type="dismissible"]:not([open])) .sidebar-shell {
transform: translateX(-100%);
}
:host([type="dismissible"][direction="rtl"]:not([open])) .sidebar-shell {
transform: translateX(100%);
}
:host([type="dismissible"]:not([open])) .app-content {
padding-inline-start: 0;
}
wa-drawer {
--size: var(--ha-sidebar-width, 256px);
--show-duration: var(--ha-animation-duration-normal);
+4 -3
View File
@@ -2,7 +2,7 @@ import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { deepEqual } from "../common/util/deep-equal";
import type { Blueprints } from "../data/blueprint";
@@ -32,6 +32,8 @@ export class HaFilterBlueprints extends LitElement {
@state() private _blueprints?: Blueprints;
@query("ha-list") private _list?: HTMLElement;
public willUpdate(properties: PropertyValues<this>) {
super.willUpdate(properties);
@@ -96,8 +98,7 @@ export class HaFilterBlueprints extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (this.narrow || !this.expanded) return;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49}px`;
this._list!.style.height = `${this.clientHeight - 49}px`;
}, 300);
}
}
+4 -3
View File
@@ -10,7 +10,7 @@ import {
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import type { CategoryRegistryEntry } from "../data/category_registry";
@@ -49,6 +49,8 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
@state() private _shouldRender = false;
@query("ha-list") private _list?: HTMLElement;
protected hassSubscribeRequiredHostProps = ["scope"];
protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
@@ -169,8 +171,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - (49 + 48)}px`;
this._list!.style.height = `${this.clientHeight - (49 + 48)}px`;
}, 300);
}
}
+4 -3
View File
@@ -1,7 +1,7 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
@@ -34,6 +34,8 @@ export class HaFilterDevices extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
public willUpdate(properties: PropertyValues<this>) {
super.willUpdate(properties);
@@ -135,8 +137,7 @@ export class HaFilterDevices extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49 - 4 - 32}px`;
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
+18 -16
View File
@@ -1,7 +1,8 @@
import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
@@ -31,6 +32,8 @@ export class HaFilterDomains extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
protected render() {
return html`
<ha-expansion-panel
@@ -62,7 +65,7 @@ export class HaFilterDomains extends LitElement {
multi
>
${repeat(
this._domains(this.hass.states, this._filter),
this._domains(this.hass.states, this._filter, this.value),
(i) => i,
(domain) =>
html`<ha-check-list-item
@@ -84,7 +87,7 @@ export class HaFilterDomains extends LitElement {
`;
}
private _domains = memoizeOne((states, filter) => {
private _domains = memoizeOne((states, filter, _value) => {
const domains = new Set<string>();
Object.keys(states).forEach((entityId) => {
domains.add(computeDomain(entityId));
@@ -109,8 +112,7 @@ export class HaFilterDomains extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49 - 4 - 32}px`;
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
@@ -126,19 +128,19 @@ export class HaFilterDomains extends LitElement {
this.expanded = ev.detail.expanded;
}
private _handleItemSelected(
ev: CustomEvent<{ diff: { added: number[]; removed: number[] } }>
) {
const domains = this._domains(this.hass.states, this._filter);
if (ev.detail.diff.added.length) {
this.value = [...(this.value || []), domains[ev.detail.diff.added[0]]];
} else if (ev.detail.diff.removed.length) {
const removedDomain = domains[ev.detail.diff.removed[0]];
this.value = this.value?.filter((value) => value !== removedDomain);
}
private _handleItemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
const domains = this._domains(this.hass.states, this._filter, this.value);
const visibleDomains = new Set(domains);
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
const selected = [...ev.detail.index]
.map((i) => domains[i])
.filter((d): d is string => !!d);
this.value = [...preserved, ...selected];
fireEvent(this, "data-table-filter-changed", {
value: this.value,
value: this.value.length ? this.value : undefined,
items: undefined,
});
}
+4 -3
View File
@@ -1,7 +1,7 @@
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeStateDomain } from "../common/entity/compute_state_domain";
@@ -36,6 +36,8 @@ export class HaFilterEntities extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
public willUpdate(properties: PropertyValues<this>) {
super.willUpdate(properties);
@@ -102,8 +104,7 @@ export class HaFilterEntities extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49 - 4 - 32}px`;
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
+4 -3
View File
@@ -1,7 +1,7 @@
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
@@ -42,6 +42,8 @@ export class HaFilterFloorAreas extends LitElement {
@state() private _shouldRender = false;
@query("ha-list-selectable") private _list?: HTMLElement;
public willUpdate(properties: PropertyValues<this>) {
super.willUpdate(properties);
@@ -207,8 +209,7 @@ export class HaFilterFloorAreas extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("ha-list-selectable")!.style.height =
`${this.clientHeight - 49}px`;
this._list!.style.height = `${this.clientHeight - 49}px`;
}, 300);
}
}
+14 -16
View File
@@ -1,7 +1,8 @@
import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
@@ -34,6 +35,8 @@ export class HaFilterIntegrations extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
protected render() {
return html`
<ha-expansion-panel
@@ -98,8 +101,7 @@ export class HaFilterIntegrations extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49 - 4 - 32}px`;
this._list!.style.height = `${this.clientHeight - 49 - 4 - 32}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
@@ -147,9 +149,7 @@ export class HaFilterIntegrations extends LitElement {
)
);
private _itemSelected(
ev: CustomEvent<{ diff: { added: number[]; removed: number[] } }>
) {
private _itemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
const integrations = this._integrations(
this.hass.localize,
this._manifests!,
@@ -157,18 +157,16 @@ export class HaFilterIntegrations extends LitElement {
this.value
);
if (ev.detail.diff.added.length) {
this.value = [
...(this.value || []),
integrations[ev.detail.diff.added[0]].domain,
];
} else if (ev.detail.diff.removed.length) {
const removedDomain = integrations[ev.detail.diff.removed[0]].domain;
this.value = this.value?.filter((val) => val !== removedDomain);
}
const visibleDomains = new Set(integrations.map((i) => i.domain));
const preserved = (this.value || []).filter((d) => !visibleDomains.has(d));
const selected = [...ev.detail.index]
.map((i) => integrations[i]?.domain)
.filter((d): d is string => !!d);
this.value = [...preserved, ...selected];
fireEvent(this, "data-table-filter-changed", {
value: this.value,
value: this.value.length ? this.value : undefined,
items: undefined,
});
}
+4 -3
View File
@@ -3,7 +3,7 @@ import type { SelectedDetail } from "@material/mwc-list";
import { mdiCog, mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
@@ -41,6 +41,8 @@ export class HaFilterLabels extends LitElement {
@state() private _filter?: string;
@query("ha-list") private _list?: HTMLElement;
private _filteredLabels = memoizeOne(
// `_value` used to recalculate the memoization when the selection changes
(labels: LabelRegistryEntry[], filter: string | undefined, _value) =>
@@ -137,8 +139,7 @@ export class HaFilterLabels extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - (49 + 48 + 32 + 4)}px`;
this._list!.style.height = `${this.clientHeight - (49 + 48 + 32 + 4)}px`;
// 49px - height of a header + 1px
// 4px - padding-top of the search-input
// 32px - height of the search input
+4 -3
View File
@@ -2,7 +2,7 @@ import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
@@ -33,6 +33,8 @@ export class HaFilterVoiceAssistants extends LitElement {
@state() private _shouldRender = false;
@query("ha-list") private _list?: HTMLElement;
protected render() {
return html`
<ha-expansion-panel
@@ -93,8 +95,7 @@ export class HaFilterVoiceAssistants extends LitElement {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49}px`;
this._list!.style.height = `${this.clientHeight - 49}px`;
}, 300);
}
}
@@ -1,7 +1,7 @@
import { mdiPlus } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { LocalizeFunc } from "../../common/translations/localize";
@@ -49,14 +49,15 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
@state() private _displayActions?: string[];
@query("ha-form") private _form?: HaForm;
public async focus() {
await this.updateComplete;
this.renderRoot.querySelector("ha-form")?.focus();
this._form?.focus();
}
public reportValidity(): boolean {
const form = this.renderRoot.querySelector<HaForm>("ha-form");
return form ? form.reportValidity() : true;
return this._form ? this._form.reportValidity() : true;
}
protected updated(changedProps: PropertyValues<this>): void {
+4 -2
View File
@@ -1,6 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
@@ -83,8 +83,10 @@ export class HaForm extends LitElement implements HaFormElement {
delegatesFocus: true,
};
@query(".root") private _root?: HTMLElement;
public reportValidity(): boolean {
const root = this.renderRoot.querySelector(".root");
const root = this._root;
if (!root) {
return true;
}
@@ -314,6 +314,7 @@ export class HaItemDisplayEditor extends LitElement {
// refocus the item after the sort
setTimeout(async () => {
await this.updateComplete;
// eslint-disable-next-line lit/prefer-query-decorators
const selectedElement = this.shadowRoot?.querySelector(
`ha-md-list-item:nth-child(${this._dragIndex! + 1})`
) as HTMLElement | null;
+1 -7
View File
@@ -86,9 +86,6 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: "show-advanced", type: Boolean })
public showAdvanced = false;
@property({ attribute: "show-service-id", type: Boolean })
public showServiceId = false;
@@ -666,10 +663,7 @@ export class HaServiceControl extends LitElement {
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
return dataField.selector &&
(!dataField.advanced ||
this.showAdvanced ||
(this._value?.data && this._value.data[dataField.key] !== undefined))
return dataField.selector
? html`<ha-settings-row .narrow=${this.narrow}>
${!showOptional
? hasOptional
+7 -4
View File
@@ -2,7 +2,7 @@ import { mdiStarFourPoints } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import type {
@@ -52,6 +52,10 @@ export class HaSuggestWithAIButton extends LitElement {
@state()
private _minWidth?: string;
@query("ha-assist-chip") private _chip?: HTMLElement & {
offsetWidth: number;
};
private _intervalId?: number;
protected firstUpdated(_changedProperties: PropertyValues<this>): void {
@@ -109,9 +113,8 @@ export class HaSuggestWithAIButton extends LitElement {
}
// Capture current width before changing state
const chip = this.shadowRoot?.querySelector("ha-assist-chip");
if (chip) {
this._minWidth = `${chip.offsetWidth}px`;
if (this._chip) {
this._minWidth = `${this._chip.offsetWidth}px`;
}
// Reset to suggesting state
+1
View File
@@ -486,6 +486,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
fireEvent(this, "value-changed", { value });
// eslint-disable-next-line lit/prefer-query-decorators
this.shadowRoot
?.querySelector(
`ha-target-picker-item-group[type='${this._newTarget?.type}']`
+4 -5
View File
@@ -2,7 +2,7 @@ import { consume, type ContextType } from "@lit/context";
import { mdiDeleteOutline, mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../common/dom/fire_event";
import { internationalizationContext } from "../../data/context";
@@ -67,6 +67,8 @@ class HaInputMulti extends LitElement {
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: ContextType<typeof internationalizationContext>;
@query("ha-input[data-last]") private _lastInput?: HaInput;
protected render() {
return html`
<ha-sortable
@@ -163,10 +165,7 @@ class HaInputMulti extends LitElement {
const items = [...this._items, ""];
this._fireChanged(items);
await this.updateComplete;
const field = this.shadowRoot?.querySelector(`ha-input[data-last]`) as
| HaInput
| undefined;
field?.focus();
this._lastInput?.focus();
}
private async _editItem(ev: Event) {
+4 -2
View File
@@ -1,6 +1,6 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import "../ha-ripple";
import { HaListItemBase } from "./ha-list-item-base";
@@ -34,8 +34,10 @@ export class HaListItemButton extends HaListItemBase {
@property({ type: String }) public download?: string;
@query("#item") private _item?: HTMLElement;
public override activate(): void {
this.renderRoot.querySelector<HTMLElement>("#item")?.click();
this._item?.click();
}
protected _renderBase(inner: TemplateResult): TemplateResult {
+8 -6
View File
@@ -12,7 +12,7 @@ import type {
} from "leaflet";
import type { PropertyValues } from "lit";
import { css, ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { formatDateTime } from "../../common/datetime/format_date_time";
import {
formatTimeWeekday,
@@ -105,6 +105,8 @@ export class HaMap extends ReactiveElement {
@state() private _loaded = false;
@query("#map") private _mapElement?: HTMLElement;
public leafletMap?: Map;
private Leaflet?: LeafletModuleType;
@@ -235,11 +237,11 @@ export class HaMap extends ReactiveElement {
}
private _updateMapStyle(): void {
const map = this.renderRoot.querySelector("#map");
map!.classList.toggle("clickable", this.clickable);
map!.classList.toggle("dark", this._darkMode);
map!.classList.toggle("forced-dark", this.themeMode === "dark");
map!.classList.toggle("forced-light", this.themeMode === "light");
const map = this._mapElement!;
map.classList.toggle("clickable", this.clickable);
map.classList.toggle("dark", this._darkMode);
map.classList.toggle("forced-dark", this.themeMode === "dark");
map.classList.toggle("forced-light", this.themeMode === "light");
}
private _loading = false;
+8 -7
View File
@@ -20,7 +20,7 @@ import {
} from "@mdi/js";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { Condition, Trigger } from "../../data/automation";
@@ -73,6 +73,9 @@ export class HatScriptGraph extends LitElement {
@property({ attribute: false }) public selected?: string;
@query("hat-graph-node[active], hat-graph-branch[active]")
private _activeNode?: HTMLElement;
public hass!: HomeAssistant;
public renderedNodes: Record<string, NodeInfo> = {};
@@ -667,12 +670,10 @@ export class HatScriptGraph extends LitElement {
// Scroll to active node when selection changes
if (changedProps.has("selected")) {
const activeNode = this.renderRoot.querySelector(
"hat-graph-node[active], hat-graph-branch[active]"
) as HTMLElement;
if (activeNode) {
activeNode.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
this._activeNode?.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}
if (!changedProps.has("trace")) {
@@ -120,7 +120,6 @@ class MoreInfoScript extends LitElement {
...(this.data ? { data: this.data } : {}),
...this._scriptData,
}}
.showAdvanced=${this.hass.userData?.showAdvanced}
.narrow=${this.narrow}
@value-changed=${this._scriptDataChanged}
></ha-service-control>
+2 -4
View File
@@ -214,10 +214,8 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
if (
changedProperties.has("tabs") ||
(changedProperties.has("hass") &&
(this.hass?.config.components !==
changedProperties.get("hass")?.config.components ||
this.hass?.userData?.showAdvanced !==
changedProperties.get("hass")?.userData?.showAdvanced))
this.hass?.config.components !==
changedProperties.get("hass")?.config.components)
) {
this.showTabs =
this.tabs.filter((page) => canShowPage(this.hass, page)).length > 1;
-1
View File
@@ -33,7 +33,6 @@ export interface PageNavigation {
name?: string;
not_component?: string | string[];
core?: boolean;
advancedOnly?: boolean;
/** Hide from non-admin users in filtered navigation and quick bar. */
adminOnly?: boolean;
iconPath?: string;
+4 -5
View File
@@ -1,6 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { LOCAL_TIME_ZONE } from "../common/datetime/resolve-time-zone";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
@@ -42,6 +42,8 @@ class OnboardingCoreConfig extends LitElement {
@state() private _skipCore = false;
@query("ha-country-picker") private _countryPicker?: HTMLElement;
protected render(): TemplateResult {
if (!this._location) {
return html`<onboarding-location
@@ -143,10 +145,7 @@ class OnboardingCoreConfig extends LitElement {
fireEvent(this, "onboarding-progress", { increase: 0.5 });
await this.updateComplete;
setTimeout(
() => this.renderRoot.querySelector("ha-country-picker")!.focus(),
100
);
setTimeout(() => this._countryPicker!.focus(), 100);
}
private async _save(ev) {
+3 -1
View File
@@ -64,6 +64,8 @@ class OnboardingLocation extends LitElement {
@query("ha-locations-editor", true) private map!: HaLocationsEditor;
@query("ha-input") private _input?: HTMLElement;
protected render(): TemplateResult {
const addressAttribution = this.onboardingLocalize(
"ui.panel.page-onboarding.core-config.location_address",
@@ -201,7 +203,7 @@ class OnboardingLocation extends LitElement {
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
setTimeout(() => this.renderRoot.querySelector("ha-input")!.focus(), 100);
setTimeout(() => this._input!.focus(), 100);
this.addEventListener("keyup", (ev) => {
if (ev.key === "Enter") {
this._save(ev);
+4 -2
View File
@@ -15,7 +15,7 @@ import {
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoize from "memoize-one";
import { firstWeekdayIndex } from "../../common/datetime/first_weekday";
import { resolveTimeZone } from "../../common/datetime/resolve-time-zone";
@@ -103,6 +103,8 @@ export class HAFullCalendar extends LitElement {
@state() private _activeView = this.initialView;
@query("style[data-fullcalendar]") private _fullCalendarStyle?: HTMLElement;
// @ts-ignore
private _resizeController = new ResizeController(this, {
callback: () => this.calendar?.updateSize(),
@@ -113,7 +115,7 @@ export class HAFullCalendar extends LitElement {
super.disconnectedCallback();
this.calendar?.destroy();
this.calendar = undefined;
this.renderRoot.querySelector("style[data-fullcalendar]")?.remove();
this._fullCalendarStyle?.remove();
}
connectedCallback(): void {
@@ -85,7 +85,6 @@ export class HaServiceAction extends LitElement implements ActionElement {
.hass=${this.hass}
.value=${this._action}
.disabled=${this.disabled}
.showAdvanced=${this.hass.userData?.showAdvanced}
.hidePicker=${!!this._action.metadata}
@value-changed=${this._actionChanged}
></ha-service-control>
@@ -36,6 +36,7 @@ import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
import type {
AutomationConfig,
AutomationEntity,
@@ -121,6 +122,8 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
@query("manual-automation-editor")
private _manualEditor?: HaManualAutomationEditor;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _configSubscriptions: Record<
string,
(config?: AutomationConfig) => void
@@ -827,7 +830,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
this._yamlEditor?.setValue(this.config);
}
this.readOnly = true;
this.errors = undefined;
@@ -1,7 +1,7 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
@@ -92,6 +92,8 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
@state() private _config?: BackupConfig;
@query("div") private _copyContainer?: HTMLElement;
public showDialog(params: BackupOnboardingDialogParams): void {
this._params = params;
@@ -478,7 +480,7 @@ class DialogBackupOnboarding extends LitElement implements HassDialog {
private async _copyKeyToClipboard() {
await copyToClipboard(
this._config!.create_backup.password!,
this.renderRoot.querySelector("div")!
this._copyContainer!
);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
@@ -1,7 +1,7 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
@@ -37,6 +37,8 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
@state() private _newEncryptionKey?: string;
@query("div") private _copyContainer?: HTMLElement;
public showDialog(params: ChangeBackupEncryptionKeyDialogParams): void {
this._params = params;
this._step = STEPS[0];
@@ -233,10 +235,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
}
private async _copyKeyToClipboard() {
await copyToClipboard(
this._newEncryptionKey,
this.renderRoot.querySelector("div")!
);
await copyToClipboard(this._newEncryptionKey, this._copyContainer!);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
@@ -246,10 +245,7 @@ class DialogChangeBackupEncryptionKey extends LitElement implements HassDialog {
if (!this._params?.currentKey) {
return;
}
await copyToClipboard(
this._params.currentKey,
this.renderRoot.querySelector("div")!
);
await copyToClipboard(this._params.currentKey, this._copyContainer!);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
@@ -1,7 +1,7 @@
import { mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
@@ -36,6 +36,8 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
@state() private _newEncryptionKey?: string;
@query("div") private _copyContainer?: HTMLElement;
public showDialog(params: SetBackupEncryptionKeyDialogParams): void {
this._params = params;
this._step = STEPS[0];
@@ -178,10 +180,7 @@ class DialogSetBackupEncryptionKey extends LitElement implements HassDialog {
}
private async _copyKeyToClipboard() {
await copyToClipboard(
this._newEncryptionKey,
this.renderRoot.querySelector("div")!
);
await copyToClipboard(this._newEncryptionKey, this._copyContainer!);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
@@ -1,7 +1,7 @@
import { mdiClose, mdiContentCopy, mdiDownload } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/ha-button";
@@ -26,6 +26,8 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
@state() private _params?: ShowBackupEncryptionKeyDialogParams;
@query("div") private _copyContainer?: HTMLElement;
public showDialog(params: ShowBackupEncryptionKeyDialogParams): void {
this._params = params;
this._open = true;
@@ -105,10 +107,7 @@ class DialogShowBackupEncryptionKey extends LitElement implements HassDialog {
if (!this._params?.currentKey) {
return;
}
await copyToClipboard(
this._params?.currentKey,
this.renderRoot.querySelector("div")!
);
await copyToClipboard(this._params?.currentKey, this._copyContainer!);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
@@ -250,6 +250,7 @@ class ConfigAnalytics extends SubscribeMixin(LitElement) {
}
private _scrollToSection(section: string): void {
// eslint-disable-next-line lit/prefer-query-decorators
const card = this.shadowRoot?.querySelector(
`[data-section="${section}"]`
) as HTMLElement;
@@ -203,7 +203,6 @@ class HaPanelDevAction extends MatchMinHeightMixin(LitElement) {
.hass=${this.hass}
.value=${this._serviceData}
.narrow=${this.narrow}
show-advanced
show-service-id
@value-changed=${this._serviceDataChanged}
class="card-content ui-mode-content"
@@ -1,7 +1,7 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { debounce } from "../../../../common/util/debounce";
@@ -58,10 +58,14 @@ class HaPanelDevTemplate extends LitElement {
@state() private _descriptionExpanded = false;
@query("ha-tip") private _editorTip?: HTMLElement;
private _template = "";
private _inited = false;
private _tipResizeObserver?: ResizeObserver;
public connectedCallback() {
super.connectedCallback();
if (this._template && !this._unsubRenderTemplate) {
@@ -72,6 +76,8 @@ class HaPanelDevTemplate extends LitElement {
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribeTemplate();
this._tipResizeObserver?.disconnect();
this._tipResizeObserver = undefined;
}
protected firstUpdated() {
@@ -81,6 +87,7 @@ class HaPanelDevTemplate extends LitElement {
this._template = DEMO_TEMPLATE;
}
this._subscribeTemplate();
this._observeTipHeight();
this._inited = true;
}
@@ -288,6 +295,21 @@ ${type === "object"
`;
}
private _observeTipHeight() {
if (!this._editorTip || this._tipResizeObserver) {
return;
}
this._tipResizeObserver = new ResizeObserver((entries) => {
const height =
entries[0]?.borderBoxSize?.[0]?.blockSize ??
entries[0]?.contentRect.height;
if (height) {
this.style.setProperty("--tip-height", `${height}px`);
}
});
this._tipResizeObserver.observe(this._editorTip);
}
private _expandedChanged(
ev: HASSDomEvent<HASSDomEvents["expanded-changed"]>
) {
@@ -331,6 +353,9 @@ ${type === "object"
var(--ha-card-header-font-size, var(--ha-font-size-2xl))
);
--card-actions-height: calc(1px + var(--ha-space-2) * 2 + 40px);
--tip-height-minimal: calc(
var(--mdc-icon-size, 24px) + var(--ha-space-4)
);
--edit-pane-height: calc(
100vh - var(--panel-header-height) - var(
--description-pane-height
@@ -340,8 +365,9 @@ ${type === "object"
--code-mirror-max-height: calc(
var(--edit-pane-height) - var(--card-header-height) +
var(--ha-space-2) - var(--card-actions-height) - var(
--ha-space-4
) - var(--ha-card-border-width, 1px) *
--tip-height,
var(--tip-height-minimal)
) - var(--ha-space-4) - var(--ha-card-border-width, 1px) *
2
);
}
@@ -143,15 +143,17 @@ export class HaConfigDeviceDashboard extends LitElement {
private _filter: string = history.state?.filter || "";
@state()
private _filters: DataTableFilters = {};
@storage({
storage: "sessionStorage",
key: "devices-table-filters-full",
state: true,
state: false,
subscribe: false,
serializer: serializeFilters,
deserializer: deserializeFilters,
})
private _filters: DataTableFilters = {};
private _storageFilters: DataTableFilters = {};
@state() private _expandedFilter?: string;
@@ -188,6 +190,8 @@ export class HaConfigDeviceDashboard extends LitElement {
private _ignoreLocationChange = false;
private _fromUrl = false;
public connectedCallback() {
super.connectedCallback();
window.addEventListener("location-changed", this._locationChanged);
@@ -228,6 +232,7 @@ export class HaConfigDeviceDashboard extends LitElement {
willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._filters = this._storageFilters;
this._setFiltersFromUrl();
}
if (changedProps.has("_selected")) {
@@ -253,6 +258,7 @@ export class HaConfigDeviceDashboard extends LitElement {
return;
}
this._fromUrl = true;
this._filter = history.state?.filter || "";
this._filters = {
@@ -295,6 +301,9 @@ export class HaConfigDeviceDashboard extends LitElement {
private _clearFilter() {
this._filters = {};
if (!this._fromUrl) {
this._storageFilters = {};
}
}
private _devicesAndFilterDomains = memoizeOne(
@@ -948,6 +957,9 @@ export class HaConfigDeviceDashboard extends LitElement {
private _filterChanged(ev) {
const type = ev.target.localName;
this._filters = { ...this._filters, [type]: ev.detail };
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
}
private _batteryEntity(
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-button";
@@ -53,6 +53,8 @@ export class DialogEnergyBatterySettings
@state() private _error?: string;
@query("ha-energy-power-config") private _powerConfigEl?: HaEnergyPowerConfig;
private _excludeList?: string[];
private _excludeListPower?: string[];
@@ -223,10 +225,7 @@ export class DialogEnergyBatterySettings
}
// Check power config validity
const powerConfigEl = this.shadowRoot?.querySelector(
"ha-energy-power-config"
) as HaEnergyPowerConfig | null;
if (powerConfigEl && !powerConfigEl.isValid()) {
if (this._powerConfigEl && !this._powerConfigEl.isValid()) {
return false;
}
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
@@ -63,6 +63,8 @@ export class DialogEnergyGridSettings
@state() private _error?: string;
@query("ha-energy-power-config") private _powerConfigEl?: HaEnergyPowerConfig;
private _excludeList?: string[];
private _excludeListPower?: string[];
@@ -434,10 +436,7 @@ export class DialogEnergyGridSettings
// Check power config validity (if power is configured)
if (hasPower) {
const powerConfigEl = this.shadowRoot?.querySelector(
"ha-energy-power-config"
) as HaEnergyPowerConfig | null;
if (powerConfigEl && !powerConfigEl.isValid()) {
if (this._powerConfigEl && !this._powerConfigEl.isValid()) {
return false;
}
}
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeAreaName } from "../../../../common/entity/compute_area_name";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
@@ -46,6 +46,9 @@ export class DialogVacuumSegmentMapping
@state() private _submitting = false;
@query("ha-vacuum-segment-area-mapper")
private _mapper?: HaVacuumSegmentAreaMapper;
private _entry?: ExtEntityRegistryEntry;
public async showDialog(
@@ -91,9 +94,7 @@ export class DialogVacuumSegmentMapping
this._submitting = true;
try {
const mapper = this.renderRoot.querySelector(
"ha-vacuum-segment-area-mapper"
) as HaVacuumSegmentAreaMapper;
const mapper = this._mapper!;
const options: VacuumEntityOptions = {
...(this._entry?.options?.vacuum ?? {}),
-1
View File
@@ -414,7 +414,6 @@ export const configSections: Record<string, PageNavigation[]> = {
iconPath: mdiBadgeAccountHorizontal,
iconColor: "#5A87FA",
core: true,
advancedOnly: true,
adminOnly: true,
},
],
@@ -1,7 +1,7 @@
import { mdiAlertOutline } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
@@ -120,6 +120,9 @@ export class DialogHelperDetail extends LitElement {
@state() private _filter?: string;
@query("ha-input-search")
private _searchInput?: HTMLElement & { updateComplete?: Promise<unknown> };
private _pendingConfigFlow?: {
startFlowHandler: string;
manifest: IntegrationManifest;
@@ -429,9 +432,7 @@ export class DialogHelperDetail extends LitElement {
}
private async _focusSearchInput() {
const searchInput = this.shadowRoot?.querySelector("ha-input-search") as
| (HTMLElement & { updateComplete?: Promise<unknown> })
| null;
const searchInput = this._searchInput;
if (!searchInput) {
return;
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-picker";
@@ -35,6 +35,8 @@ class HaCounterForm extends LitElement {
@state() private _step?: number;
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
set item(item: Counter) {
this._item = item;
if (item) {
@@ -57,11 +59,7 @@ class HaCounterForm extends LitElement {
}
public focus() {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
this.updateComplete.then(() => this._focusElement?.focus());
}
protected render() {
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-picker";
import "../../../../components/input/ha-input";
@@ -22,6 +22,8 @@ class HaInputBooleanForm extends LitElement {
@state() private _icon!: string;
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
set item(item: InputBoolean) {
this._item = item;
if (item) {
@@ -34,11 +36,7 @@ class HaInputBooleanForm extends LitElement {
}
public focus() {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
this.updateComplete.then(() => this._focusElement?.focus());
}
protected render() {
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-picker";
import "../../../../components/input/ha-input";
@@ -20,6 +20,8 @@ class HaInputButtonForm extends LitElement {
@state() private _icon!: string;
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
private _item?: InputButton;
set item(item: InputButton) {
@@ -34,11 +36,7 @@ class HaInputButtonForm extends LitElement {
}
public focus() {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
this.updateComplete.then(() => this._focusElement?.focus());
}
protected render() {
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-picker";
import "../../../../components/input/ha-input";
@@ -27,6 +27,8 @@ class HaInputDateTimeForm extends LitElement {
@state() private _mode!: "date" | "time" | "datetime";
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
set item(item: InputDateTime) {
this._item = item;
if (item) {
@@ -48,11 +50,7 @@ class HaInputDateTimeForm extends LitElement {
}
public focus() {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
this.updateComplete.then(() => this._focusElement?.focus());
}
protected render() {
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-icon-picker";
import "../../../../components/radio/ha-radio-group";
@@ -49,6 +49,8 @@ class HaInputNumberForm extends LitElement {
// eslint-disable-next-line: variable-name
@state() private _unit_of_measurement?: string;
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
/* Configuring initial value is intentionally not supported because the behavior
compared to restoring the value after restart is hard to explain */
set item(item: InputNumber) {
@@ -76,11 +78,7 @@ class HaInputNumberForm extends LitElement {
}
public focus() {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
this.updateComplete.then(() => this._focusElement?.focus());
}
protected render() {
@@ -36,6 +36,8 @@ class HaInputSelectForm extends LitElement {
@query("#option_input", true) private _optionInput?: HaInput;
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
private _optionMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
@@ -62,11 +64,7 @@ class HaInputSelectForm extends LitElement {
}
public focus() {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
this.updateComplete.then(() => this._focusElement?.focus());
}
protected render() {
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form";
@@ -35,6 +35,8 @@ class HaInputTextForm extends LitElement {
@state() private _pattern?: string;
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
set item(item: InputText) {
this._item = item;
if (item) {
@@ -54,11 +56,7 @@ class HaInputTextForm extends LitElement {
}
public focus() {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
this.updateComplete.then(() => this._focusElement?.focus());
}
protected render() {
@@ -7,7 +7,7 @@ import type { Day } from "date-fns";
import { addDays, isSameDay, isSameWeek, nextDay } from "date-fns";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { firstWeekdayIndex } from "../../../../common/datetime/first_weekday";
import { formatTime24h } from "../../../../common/datetime/format_time";
import { useAmPm } from "../../../../common/datetime/use_am_pm";
@@ -65,6 +65,10 @@ class HaScheduleForm extends LitElement {
@state() private calendar?: Calendar;
@query("style[data-fullcalendar]") private _fullCalendarStyle?: HTMLElement;
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
private _item?: Schedule;
set item(item: Schedule) {
@@ -96,7 +100,7 @@ class HaScheduleForm extends LitElement {
super.disconnectedCallback();
this.calendar?.destroy();
this.calendar = undefined;
this.renderRoot.querySelector("style[data-fullcalendar]")?.remove();
this._fullCalendarStyle?.remove();
}
public connectedCallback(): void {
@@ -107,11 +111,7 @@ class HaScheduleForm extends LitElement {
}
public focus() {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
this.updateComplete.then(() => this._focusElement?.focus());
}
protected render() {
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { createDurationData } from "../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-checkbox";
@@ -33,6 +33,8 @@ class HaTimerForm extends LitElement {
@state() private _restore!: boolean;
@query("[dialogInitialFocus]") private _focusElement?: HTMLElement;
set item(item: Timer) {
this._item = item;
if (item) {
@@ -51,11 +53,7 @@ class HaTimerForm extends LitElement {
}
public focus() {
this.updateComplete.then(() =>
(
this.shadowRoot?.querySelector("[dialogInitialFocus]") as HTMLElement
)?.focus()
);
this.updateComplete.then(() => this._focusElement?.focus());
}
protected render() {
@@ -17,7 +17,7 @@ import {
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { goBack } from "../../../../../common/navigate";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
@@ -84,6 +84,8 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
@state() private _multipleNetworks = false;
@query("#nvm-restore-file") private _restoreFileInput?: HTMLInputElement;
private _dialogOpen = false;
private _s2InclusionUnsubscribe?: Promise<UnsubscribeFunc>;
@@ -702,10 +704,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) {
}
private _restoreButtonClick() {
const fileInput = this.shadowRoot?.querySelector(
"#nvm-restore-file"
) as HTMLInputElement;
fileInput?.click();
this._restoreFileInput?.click();
}
private async _handleRestoreFileSelected(ev: Event) {
+1
View File
@@ -277,6 +277,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
}
private _scrollToPreviewFeature(previewFeatureId: string): void {
// eslint-disable-next-line lit/prefer-query-decorators
const card = this.shadowRoot?.querySelector(
`[data-feature-id="${previewFeatureId}"]`
) as HTMLElement;
@@ -1,7 +1,7 @@
import { mdiContentCopy } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
import "../../../components/ha-alert";
@@ -35,6 +35,8 @@ class DialogSystemLogDetail extends LitElement {
@state() private _open = false;
@query(".contents") private _contents?: HTMLElement;
public async showDialog(params: SystemLogDetailDialogParams): Promise<void> {
this._params = params;
this._manifest = undefined;
@@ -205,9 +207,7 @@ class DialogSystemLogDetail extends LitElement {
}
private async _copyLog(): Promise<void> {
const copyElement = this.shadowRoot?.querySelector(
".contents"
) as HTMLElement;
const copyElement = this._contents!;
let text = copyElement.innerText;
+4 -1
View File
@@ -34,6 +34,7 @@ import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
import { substituteBlueprint } from "../../../data/blueprint";
import { validateConfig } from "../../../data/config";
import { UNAVAILABLE } from "../../../data/entity/entity";
@@ -90,6 +91,8 @@ export class HaScriptEditor extends SubscribeMixin(
@query("manual-script-editor")
private _manualEditor?: HaManualScriptEditor;
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
private _newScriptId?: string;
protected domainHooks: EditorDomainHooks<ScriptConfig> = {
@@ -750,7 +753,7 @@ export class HaScriptEditor extends SubscribeMixin(
this.blueprintConfig = config;
this.config = newConfig;
if (this.mode === "yaml") {
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
this._yamlEditor?.setValue(this.config);
}
this.readOnly = true;
this.errors = undefined;
@@ -1,5 +1,5 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -15,10 +15,11 @@ export class AssistPipelineDetailConfig extends LitElement {
@property({ attribute: false })
public supportedLanguages?: string[];
@query("ha-form") private _form?: HTMLElement;
public async focus() {
await this.updateComplete;
const input = this.renderRoot?.querySelector("ha-form");
input?.focus();
this._form?.focus();
}
private _schema = memoizeOne(
+1
View File
@@ -433,6 +433,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
private async _editZone(id: string) {
await this.updateComplete;
// eslint-disable-next-line lit/prefer-query-decorators
(this.shadowRoot?.querySelector(`[id="${id}"]`) as HTMLElement)?.click();
}
+5 -4
View File
@@ -2,7 +2,7 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
import { mdiPencil } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { atLeastVersion } from "../../common/config/version";
import { navigate } from "../../common/navigate";
@@ -48,6 +48,8 @@ class PanelHome extends LitElement {
@state() private _extraActionItems?: ExtraActionItem[];
@query(".banner") private _banner?: HTMLElement;
private _loadConfigPromise?: Promise<void>;
private get _showBanner(): boolean {
@@ -299,9 +301,8 @@ class PanelHome extends LitElement {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("_showBanner") || changedProps.has("_lovelace")) {
const banner = this.shadowRoot?.querySelector(".banner");
if (banner) {
this._bannerHeight.observe(banner);
if (this._banner) {
this._bannerHeight.observe(this._banner);
}
}
}
@@ -0,0 +1,56 @@
import { mdiVolumeHigh, mdiVolumeOff } from "@mdi/js";
import { html, nothing } from "lit";
import type { TemplateResult } from "lit";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import "../../../../components/ha-control-button";
import "../../../../components/ha-svg-icon";
import { forwardHaptic } from "../../../../data/haptics";
import {
MediaPlayerEntityFeature,
type MediaPlayerEntity,
} from "../../../../data/media-player";
import type { HomeAssistant } from "../../../../types";
export const renderMuteButton = (
hass: HomeAssistant,
stateObj: MediaPlayerEntity,
showMuteButton: boolean | undefined,
disabled: boolean,
onToggleMute: (ev: Event) => void
): TemplateResult | typeof nothing => {
if (
!(showMuteButton ?? true) ||
!supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_MUTE)
) {
return nothing;
}
const isMuted = stateObj.attributes.is_volume_muted;
return html`
<ha-control-button
class="mute"
.label=${hass.localize(
`ui.card.media_player.${isMuted ? "media_volume_unmute" : "media_volume_mute"}`
)}
.disabled=${disabled}
@click=${onToggleMute}
>
<ha-svg-icon
.path=${isMuted ? mdiVolumeOff : mdiVolumeHigh}
></ha-svg-icon>
</ha-control-button>
`;
};
export const toggleMediaPlayerMute = (
ev: Event,
hass: HomeAssistant,
stateObj: MediaPlayerEntity,
el: HTMLElement
): void => {
ev.stopPropagation();
forwardHaptic(el, "light");
hass.callService("media_player", "volume_mute", {
entity_id: stateObj.entity_id,
is_volume_muted: !stateObj.attributes.is_volume_muted,
});
};
@@ -5,6 +5,11 @@ import {
mdiPower,
mdiPowerOff,
mdiPowerOn,
mdiRepeat,
mdiRepeatOff,
mdiRepeatOnce,
mdiShuffle,
mdiShuffleDisabled,
mdiSkipNext,
mdiSkipPrevious,
mdiStop,
@@ -59,6 +64,8 @@ const MEDIA_PLAYER_PLAYBACK_CONTROLS_FEATURES: Record<
volume_down: [MediaPlayerEntityFeature.VOLUME_STEP],
volume_up: [MediaPlayerEntityFeature.VOLUME_STEP],
volume_mute: [MediaPlayerEntityFeature.VOLUME_MUTE],
shuffle: [MediaPlayerEntityFeature.SHUFFLE_SET],
repeat: [MediaPlayerEntityFeature.REPEAT_SET],
};
export const supportsMediaPlayerPlaybackControl = (
@@ -293,6 +300,30 @@ class HuiMediaPlayerPlaybackCardFeature
});
}
break;
case "shuffle":
if (supportsFeature(stateObj, MediaPlayerEntityFeature.SHUFFLE_SET)) {
buttons.push({
icon:
stateObj.attributes.shuffle === true
? mdiShuffle
: mdiShuffleDisabled,
action: "shuffle",
});
}
break;
case "repeat":
if (supportsFeature(stateObj, MediaPlayerEntityFeature.REPEAT_SET)) {
buttons.push({
icon:
stateObj.attributes.repeat === "all"
? mdiRepeat
: stateObj.attributes.repeat === "one"
? mdiRepeatOnce
: mdiRepeatOff,
action: "repeat",
});
}
break;
}
}
@@ -336,6 +367,23 @@ class HuiMediaPlayerPlaybackCardFeature
return;
}
if (action === "shuffle") {
this.hass!.callService("media_player", "shuffle_set", {
entity_id: this._stateObj.entity_id,
shuffle: !this._stateObj.attributes.shuffle,
});
return;
}
if (action === "repeat") {
const repeat = this._stateObj.attributes.repeat ?? "off";
this.hass!.callService("media_player", "repeat_set", {
entity_id: this._stateObj.entity_id,
repeat: repeat === "off" ? "one" : repeat === "one" ? "all" : "off",
});
return;
}
this.hass!.callService("media_player", action, {
entity_id: this._stateObj.entity_id,
});
@@ -8,7 +8,7 @@ import {
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import { hasConfigChanged } from "../common/has-changed";
import type { LovelaceCardFeature } from "../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base";
import type {
LovelaceCardFeatureContext,
@@ -43,6 +43,11 @@ class HuiMediaPlayerSourceCardFeature
protected readonly _modesAttribute = "source_list";
protected get _configuredModes() {
const sources = this._config?.sources;
return sources?.length ? sources : undefined;
}
protected readonly _serviceDomain = "media_player";
protected readonly _serviceAction = "select_source";
@@ -63,6 +68,13 @@ class HuiMediaPlayerSourceCardFeature
};
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import("../editor/config-elements/hui-media-player-source-card-feature-editor");
return document.createElement(
"hui-media-player-source-card-feature-editor"
);
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
const entityId = this.context?.entity_id;
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
@@ -1,13 +1,9 @@
import { mdiVolumeHigh, mdiVolumeOff } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { clamp } from "../../../common/number/clamp";
import "../../../components/ha-control-button";
import "../../../components/ha-control-number-buttons";
import "../../../components/ha-svg-icon";
import { forwardHaptic } from "../../../data/haptics";
import { isUnavailableState } from "../../../data/entity/entity";
import {
MediaPlayerEntityFeature,
@@ -16,6 +12,10 @@ import {
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import {
renderMuteButton,
toggleMediaPlayerMute,
} from "./common/media-player-mute-button";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeButtonsCardFeatureConfig,
@@ -90,10 +90,6 @@ class HuiMediaPlayerVolumeButtonsCardFeature
const stateObj = this._stateObj;
const disabled = isUnavailableState(stateObj.state);
const showMute =
(this._config.show_mute_button ?? true) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_MUTE);
const isMuted = stateObj.attributes.is_volume_muted;
const position =
stateObj.attributes.volume_level != null
@@ -111,22 +107,13 @@ class HuiMediaPlayerVolumeButtonsCardFeature
unit="%"
@value-changed=${this._valueChanged}
></ha-control-number-buttons>
${showMute
? html`
<ha-control-button
class="mute"
.label=${this.hass.localize(
`ui.card.media_player.${isMuted ? "media_volume_unmute" : "media_volume_mute"}`
)}
.disabled=${disabled}
@click=${this._toggleMute}
>
<ha-svg-icon
.path=${isMuted ? mdiVolumeOff : mdiVolumeHigh}
></ha-svg-icon>
</ha-control-button>
`
: nothing}
${renderMuteButton(
this.hass,
stateObj,
this._config.show_mute_button,
disabled,
this._toggleMute
)}
`;
}
@@ -139,14 +126,9 @@ class HuiMediaPlayerVolumeButtonsCardFeature
});
}
private _toggleMute(ev: Event) {
ev.stopPropagation();
forwardHaptic(this, "light");
this.hass!.callService("media_player", "volume_mute", {
entity_id: this._stateObj!.entity_id,
is_volume_muted: !this._stateObj!.attributes.is_volume_muted,
});
}
private _toggleMute = (ev: Event) => {
toggleMediaPlayerMute(ev, this.hass!, this._stateObj!, this);
};
static get styles() {
return [
@@ -1,4 +1,4 @@
import { html, LitElement, nothing } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
@@ -10,8 +10,12 @@ import {
type MediaPlayerEntity,
} from "../../../data/media-player";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
import { cardFeatureStyles } from "./common/card-feature-styles";
import {
renderMuteButton,
toggleMediaPlayerMute,
} from "./common/media-player-mute-button";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeSliderCardFeatureConfig,
@@ -58,6 +62,13 @@ class HuiMediaPlayerVolumeSliderCardFeature
};
}
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
await import("../editor/config-elements/hui-media-player-volume-slider-card-feature-editor");
return document.createElement(
"hui-media-player-volume-slider-card-feature-editor"
);
}
public setConfig(config: MediaPlayerVolumeSliderCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
@@ -76,9 +87,12 @@ class HuiMediaPlayerVolumeSliderCardFeature
return nothing;
}
const stateObj = this._stateObj;
const disabled = isUnavailableState(stateObj.state);
const position =
this._stateObj.attributes.volume_level != null
? Math.round(this._stateObj.attributes.volume_level * 100)
stateObj.attributes.volume_level != null
? Math.round(stateObj.attributes.volume_level * 100)
: undefined;
return html`
@@ -86,12 +100,19 @@ class HuiMediaPlayerVolumeSliderCardFeature
.value=${position}
min="0"
max="100"
.showHandle=${stateActive(this._stateObj)}
.disabled=${!this._stateObj || isUnavailableState(this._stateObj.state)}
.showHandle=${stateActive(stateObj)}
.disabled=${disabled}
@value-changed=${this._valueChanged}
unit="%"
.locale=${this.hass.locale}
></ha-control-slider>
${renderMuteButton(
this.hass,
stateObj,
this._config.show_mute_button,
disabled,
this._toggleMute
)}
`;
}
@@ -105,8 +126,29 @@ class HuiMediaPlayerVolumeSliderCardFeature
});
}
private _toggleMute = (ev: Event) => {
toggleMediaPlayerMute(ev, this.hass!, this._stateObj!, this);
};
static get styles() {
return cardFeatureStyles;
return [
cardFeatureStyles,
css`
:host {
display: flex;
flex-direction: row;
gap: var(--feature-button-spacing);
}
ha-control-slider {
flex: 1;
min-width: 0;
}
.mute {
width: var(--feature-height);
height: var(--feature-height);
}
`,
];
}
}
@@ -66,6 +66,8 @@ export const MEDIA_PLAYER_PLAYBACK_CONTROLS = [
"volume_down",
"volume_up",
"volume_mute",
"shuffle",
"repeat",
] as const;
export type MediaPlayerPlaybackControl =
@@ -78,10 +80,12 @@ export interface MediaPlayerPlaybackCardFeatureConfig {
export interface MediaPlayerSourceCardFeatureConfig {
type: "media-player-source";
sources?: string[];
}
export interface MediaPlayerVolumeSliderCardFeatureConfig {
type: "media-player-volume-slider";
show_mute_button?: boolean;
}
export interface MediaPlayerVolumeButtonsCardFeatureConfig {
@@ -91,12 +91,17 @@ export function getSuggestedMax(
return suggestedMax;
}
function createYAxisLabelFormatter(
locale: FrontendLocaleData,
fractionDigits: number
) {
return (value: number): string =>
formatNumber(value, locale, { maximumFractionDigits: fractionDigits });
function createYAxisLabelFormatter(locale: FrontendLocaleData) {
let previousValue: number | undefined;
return (value: number): string => {
const maximumFractionDigits = Math.max(
1,
-Math.floor(Math.log10(Math.abs(value - (previousValue ?? value) || 1)))
);
previousValue = value;
return formatNumber(value, locale, { maximumFractionDigits });
};
}
export function getCommonOptions(
@@ -108,8 +113,7 @@ export function getCommonOptions(
compareStart?: Date,
compareEnd?: Date,
formatTotal?: (total: number) => string,
detailedDailyData = false,
yAxisFractionDigits = 1
detailedDailyData = false
): ECOption {
const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData);
const suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData);
@@ -148,7 +152,7 @@ export function getCommonOptions(
align: "left",
},
axisLabel: {
formatter: createYAxisLabelFormatter(locale, yAxisFractionDigits),
formatter: createYAxisLabelFormatter(locale),
},
splitLine: {
show: true,
@@ -10,7 +10,6 @@ import { getGraphColorByIndex } from "../../../../common/color/colors";
import { getEnergyColor } from "./common/color";
import "../../../../components/ha-card";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import type {
DeviceConsumptionEnergyPreference,
EnergyData,
@@ -76,8 +75,6 @@ export class HuiEnergyDevicesDetailGraphCard
@state() private _chartData: BarSeriesOption[] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _data?: EnergyData;
@state() private _legendData?: CustomLegendOption["data"];
@@ -160,8 +157,7 @@ export class HuiEnergyDevicesDetailGraphCard
this.hass.config,
UNIT,
this._compareStart,
this._compareEnd,
this._yAxisFractionDigits
this._compareEnd
)}
click-label-for-more-info
@dataset-hidden=${this._datasetHidden}
@@ -212,10 +208,9 @@ export class HuiEnergyDevicesDetailGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
unit: string | undefined,
compareStart: Date | undefined,
compareEnd: Date | undefined,
yAxisFractionDigits: number
unit?: string,
compareStart?: Date,
compareEnd?: Date
): ECOption => {
const commonOptions = getCommonOptions(
start,
@@ -225,9 +220,7 @@ export class HuiEnergyDevicesDetailGraphCard
unit,
compareStart,
compareEnd,
this._formatTotal,
false,
yAxisFractionDigits
this._formatTotal
);
const selected = this._legendData
@@ -316,13 +309,6 @@ export class HuiEnergyDevicesDetailGraphCard
const datasets: BarSeriesOption[] = [];
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
const { summedData, compareSummedData } = getSummedData(energyData);
const showUntracked =
@@ -345,7 +331,6 @@ export class HuiEnergyDevicesDetailGraphCard
energyData.prefs.device_consumption,
sorted_devices,
childMap,
trackY,
true
);
@@ -356,7 +341,6 @@ export class HuiEnergyDevicesDetailGraphCard
computedStyle,
processedCompareData,
consumptionCompareData,
trackY,
true
);
datasets.push(untrackedCompareData);
@@ -378,8 +362,7 @@ export class HuiEnergyDevicesDetailGraphCard
energyData.statsMetadata,
energyData.prefs.device_consumption,
sorted_devices,
childMap,
trackY
childMap
);
datasets.push(...processedData);
@@ -402,7 +385,6 @@ export class HuiEnergyDevicesDetailGraphCard
computedStyle,
processedData,
consumptionData,
trackY,
false
);
datasets.push(untrackedData);
@@ -419,7 +401,6 @@ export class HuiEnergyDevicesDetailGraphCard
}
fillDataGapsAndRoundCaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
}
@@ -427,7 +408,6 @@ export class HuiEnergyDevicesDetailGraphCard
computedStyle: CSSStyleDeclaration,
processedData,
consumptionData,
trackY: (v: number) => void,
compare: boolean
): BarSeriesOption {
const totalDeviceConsumption: Record<number, number> = {};
@@ -463,7 +443,6 @@ export class HuiEnergyDevicesDetailGraphCard
dataPoint[0] = compareTransform(new Date(ts)).getTime() + periodOffset;
}
untrackedConsumption.push(dataPoint);
trackY(value);
});
// random id to always add untracked at the end
const order = Date.now();
@@ -504,7 +483,6 @@ export class HuiEnergyDevicesDetailGraphCard
devices: DeviceConsumptionEnergyPreference[],
sorted_devices: string[],
childMap: Record<string, string[]>,
trackY: (v: number) => void,
compare = false
) {
const data: BarSeriesOption[] = [];
@@ -552,7 +530,6 @@ export class HuiEnergyDevicesDetailGraphCard
cStats?.find((cStat) => cStat.start === point.start)?.change || 0;
});
const y = point.change - sumChildren;
const dataPoint: EnergyDataPoint = [
computeStatMidpoint(
point.start,
@@ -560,11 +537,10 @@ export class HuiEnergyDevicesDetailGraphCard
period,
compare ? compareTransform : undefined
),
y,
point.change - sumChildren,
point.start,
];
consumptionData.push(dataPoint);
trackY(y);
prevStart = point.start;
}
}
@@ -9,7 +9,6 @@ import type { BarSeriesOption } from "echarts/charts";
import { getEnergyColor } from "./common/color";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import type {
EnergyData,
@@ -65,8 +64,6 @@ export class HuiEnergyGasGraphCard
@state() private _chartData: BarSeriesOption[] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@@ -142,8 +139,7 @@ export class HuiEnergyGasGraphCard
this.hass.config,
this._unit,
this._compareStart,
this._compareEnd,
this._yAxisFractionDigits
this._compareEnd
)}
chart-type="bar"
></ha-chart-base>
@@ -173,10 +169,9 @@ export class HuiEnergyGasGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
unit: string | undefined,
compareStart: Date | undefined,
compareEnd: Date | undefined,
yAxisFractionDigits: number
unit?: string,
compareStart?: Date,
compareEnd?: Date
): ECOption =>
getCommonOptions(
start,
@@ -186,9 +181,7 @@ export class HuiEnergyGasGraphCard
unit,
compareStart,
compareEnd,
this._formatTotal,
false,
yAxisFractionDigits
this._formatTotal
)
);
@@ -210,13 +203,6 @@ export class HuiEnergyGasGraphCard
const computedStyles = getComputedStyle(this);
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
if (energyData.statsCompare) {
datasets.push(
...this._processDataSet(
@@ -224,7 +210,6 @@ export class HuiEnergyGasGraphCard
energyData.statsMetadata,
gasSources,
computedStyles,
trackY,
true
)
);
@@ -245,13 +230,11 @@ export class HuiEnergyGasGraphCard
energyData.stats,
energyData.statsMetadata,
gasSources,
computedStyles,
trackY
computedStyles
)
);
fillDataGapsAndRoundCaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._total = this._processTotal(energyData.stats, gasSources);
}
@@ -278,7 +261,6 @@ export class HuiEnergyGasGraphCard
statisticsMetaData: Record<string, StatisticsMetaData>,
gasSources: GasSourceTypeEnergyPreference[],
computedStyles: CSSStyleDeclaration,
trackY: (v: number) => void,
compare = false
) {
const data: BarSeriesOption[] = [];
@@ -318,7 +300,6 @@ export class HuiEnergyGasGraphCard
point.start,
];
gasConsumptionData.push(dataPoint);
trackY(point.change);
prevStart = point.start;
}
}
@@ -9,7 +9,6 @@ import type { BarSeriesOption, LineSeriesOption } from "echarts/charts";
import { getEnergyColor } from "./common/color";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import type {
EnergyData,
@@ -67,8 +66,6 @@ export class HuiEnergySolarGraphCard
@state() private _chartData: ECOption["series"][] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@@ -141,8 +138,7 @@ export class HuiEnergySolarGraphCard
this.hass.locale,
this.hass.config,
this._compareStart,
this._compareEnd,
this._yAxisFractionDigits
this._compareEnd
)}
chart-type="bar"
></ha-chart-base>
@@ -172,9 +168,8 @@ export class HuiEnergySolarGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
compareStart: Date | undefined,
compareEnd: Date | undefined,
yAxisFractionDigits: number
compareStart?: Date,
compareEnd?: Date
): ECOption =>
getCommonOptions(
start,
@@ -184,9 +179,7 @@ export class HuiEnergySolarGraphCard
"kWh",
compareStart,
compareEnd,
this._formatTotal,
false,
yAxisFractionDigits
this._formatTotal
)
);
@@ -217,13 +210,6 @@ export class HuiEnergySolarGraphCard
const computedStyles = getComputedStyle(this);
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
if (energyData.statsCompare) {
datasets.push(
...this._processDataSet(
@@ -231,7 +217,6 @@ export class HuiEnergySolarGraphCard
energyData.statsMetadata,
solarSources,
computedStyles,
trackY,
true
)
);
@@ -252,8 +237,7 @@ export class HuiEnergySolarGraphCard
energyData.stats,
energyData.statsMetadata,
solarSources,
computedStyles,
trackY
computedStyles
)
);
@@ -267,13 +251,11 @@ export class HuiEnergySolarGraphCard
solarSources,
computedStyles.getPropertyValue("--primary-text-color"),
energyData.start,
energyData.end,
trackY
energyData.end
)
);
}
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._total = this._processTotal(energyData.stats, solarSources);
}
@@ -300,7 +282,6 @@ export class HuiEnergySolarGraphCard
statisticsMetaData: Record<string, StatisticsMetaData>,
solarSources: SolarSourceTypeEnergyPreference[],
computedStyles: CSSStyleDeclaration,
trackY: (v: number) => void,
compare = false
) {
const data: BarSeriesOption[] = [];
@@ -341,7 +322,6 @@ export class HuiEnergySolarGraphCard
point.start,
];
solarProductionData.push(dataPoint);
trackY(point.change);
prevStart = point.start;
}
}
@@ -395,8 +375,7 @@ export class HuiEnergySolarGraphCard
solarSources: SolarSourceTypeEnergyPreference[],
borderColor: string,
start: Date,
end: Date | undefined,
trackY: (v: number) => void
end?: Date
) {
const data: LineSeriesOption[] = [];
@@ -450,9 +429,10 @@ export class HuiEnergySolarGraphCard
: 0;
}
for (const [time, value] of Object.entries(forecastsData)) {
const kWh = value / 1000;
solarForecastData.push([Number(time) + forecastOffset, kWh]);
trackY(kWh);
solarForecastData.push([
Number(time) + forecastOffset,
value / 1000,
]);
}
if (solarForecastData.length) {
@@ -13,7 +13,6 @@ import type {
import { getEnergyColor } from "./common/color";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import "./common/hui-energy-graph-chip";
import type {
@@ -80,8 +79,6 @@ export class HuiEnergyUsageGraphCard
@state() private _chartData: BarSeriesOption[] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@@ -157,8 +154,7 @@ export class HuiEnergyUsageGraphCard
this.hass.locale,
this.hass.config,
this._compareStart,
this._compareEnd,
this._yAxisFractionDigits
this._compareEnd
)}
chart-type="bar"
></ha-chart-base>
@@ -193,9 +189,8 @@ export class HuiEnergyUsageGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
compareStart: Date | undefined,
compareEnd: Date | undefined,
yAxisFractionDigits: number
compareStart?: Date,
compareEnd?: Date
): ECOption => {
const commonOptions = getCommonOptions(
start,
@@ -205,9 +200,7 @@ export class HuiEnergyUsageGraphCard
"kWh",
compareStart,
compareEnd,
this._formatTotal,
false,
yAxisFractionDigits
this._formatTotal
);
const options: ECOption = {
...commonOptions,
@@ -244,13 +237,6 @@ export class HuiEnergyUsageGraphCard
private async _getStatistics(energyData: EnergyData): Promise<void> {
const datasets: BarSeriesOption[] = [];
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
const statIds: {
to_grid?: string[];
from_grid?: string[];
@@ -355,7 +341,6 @@ export class HuiEnergyUsageGraphCard
colorIndices,
computedStyles,
labels,
trackY,
true
)
);
@@ -382,7 +367,6 @@ export class HuiEnergyUsageGraphCard
colorIndices,
computedStyles,
labels,
trackY,
false
)
);
@@ -390,7 +374,6 @@ export class HuiEnergyUsageGraphCard
// @ts-expect-error
datasets.sort((a, b) => a.order - b.order);
fillDataGapsAndRoundCaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._total = this._processTotal(consumption);
}
@@ -420,7 +403,6 @@ export class HuiEnergyUsageGraphCard
used_solar: string;
used_battery: string;
},
trackY: (v: number) => void,
compare = false
) {
const data: BarSeriesOption[] = [];
@@ -522,17 +504,18 @@ export class HuiEnergyUsageGraphCard
// Process chart data.
for (const key of uniqueKeys) {
const value = source[key] || 0;
const y =
const dataPoint: EnergyDataPoint = [
key + periodOffset,
value && ["to_grid", "to_battery"].includes(type)
? -1 * value
: value;
const dataPoint: EnergyDataPoint = [key + periodOffset, y, key];
: value,
key,
];
if (compare) {
dataPoint[0] =
compareTransform(new Date(key)).getTime() + periodOffset;
}
points.push(dataPoint);
trackY(y);
}
data.push({
@@ -8,7 +8,6 @@ import memoizeOne from "memoize-one";
import type { BarSeriesOption } from "echarts/charts";
import { getEnergyColor } from "./common/color";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import type {
EnergyData,
@@ -65,8 +64,6 @@ export class HuiEnergyWaterGraphCard
@state() private _chartData: BarSeriesOption[] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@@ -142,8 +139,7 @@ export class HuiEnergyWaterGraphCard
this.hass.config,
this._unit,
this._compareStart,
this._compareEnd,
this._yAxisFractionDigits
this._compareEnd
)}
chart-type="bar"
></ha-chart-base>
@@ -173,10 +169,9 @@ export class HuiEnergyWaterGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
unit: string | undefined,
compareStart: Date | undefined,
compareEnd: Date | undefined,
yAxisFractionDigits: number
unit?: string,
compareStart?: Date,
compareEnd?: Date
): ECOption =>
getCommonOptions(
start,
@@ -186,9 +181,7 @@ export class HuiEnergyWaterGraphCard
unit,
compareStart,
compareEnd,
this._formatTotal,
false,
yAxisFractionDigits
this._formatTotal
)
);
@@ -210,13 +203,6 @@ export class HuiEnergyWaterGraphCard
const computedStyles = getComputedStyle(this);
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
if (energyData.statsCompare) {
datasets.push(
...this._processDataSet(
@@ -224,7 +210,6 @@ export class HuiEnergyWaterGraphCard
energyData.statsMetadata,
waterSources,
computedStyles,
trackY,
true
)
);
@@ -245,13 +230,11 @@ export class HuiEnergyWaterGraphCard
energyData.stats,
energyData.statsMetadata,
waterSources,
computedStyles,
trackY
computedStyles
)
);
fillDataGapsAndRoundCaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._total = this._processTotal(energyData.stats, waterSources);
}
@@ -278,7 +261,6 @@ export class HuiEnergyWaterGraphCard
statisticsMetaData: Record<string, StatisticsMetaData>,
waterSources: WaterSourceTypeEnergyPreference[],
computedStyles: CSSStyleDeclaration,
trackY: (v: number) => void,
compare = false
) {
const data: BarSeriesOption[] = [];
@@ -318,7 +300,6 @@ export class HuiEnergyWaterGraphCard
point.start,
];
waterConsumptionData.push(dataPoint);
trackY(point.change);
prevStart = point.start;
}
}
@@ -8,7 +8,6 @@ import memoizeOne from "memoize-one";
import type { LineSeriesOption } from "echarts/charts";
import { LinearGradient } from "../../../../resources/echarts/echarts";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import type { EnergyData } from "../../../../data/energy";
import {
@@ -54,8 +53,6 @@ export class HuiPowerSourcesGraphCard
@state() private _chartData: LineSeriesOption[] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _legendData?: CustomLegendOption["data"];
@state() private _start = startOfToday();
@@ -120,8 +117,7 @@ export class HuiPowerSourcesGraphCard
this.hass.config,
this._compareStart,
this._compareEnd,
this._legendData,
this._yAxisFractionDigits
this._legendData
)}
></ha-chart-base>
${!this._chartData.some((dataset) => dataset.data!.length)
@@ -144,10 +140,9 @@ export class HuiPowerSourcesGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
compareStart: Date | undefined,
compareEnd: Date | undefined,
legendData: CustomLegendOption["data"] | undefined,
yAxisFractionDigits: number
compareStart?: Date,
compareEnd?: Date,
legendData?: CustomLegendOption["data"]
): ECOption => ({
...getCommonOptions(
start,
@@ -158,8 +153,7 @@ export class HuiPowerSourcesGraphCard
compareStart,
compareEnd,
undefined,
true,
yAxisFractionDigits
true
),
legend: {
show: this._config?.show_legend !== false,
@@ -199,13 +193,6 @@ export class HuiPowerSourcesGraphCard
const computedStyles = getComputedStyle(this);
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
for (const source of energyData.prefs.energy_sources) {
if (source.type === "solar") {
if (source.stat_rate) {
@@ -258,8 +245,7 @@ export class HuiPowerSourcesGraphCard
}
}
return stats;
}),
trackY
})
);
datasets.push({
...commonSeriesOptions,
@@ -321,7 +307,6 @@ export class HuiPowerSourcesGraphCard
this._end = energyData.end || endOfToday();
this._chartData = fillLineGaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
const usageData: NonNullable<LineSeriesOption["data"]> = [];
this._chartData[0]?.data!.forEach((item, i) => {
@@ -368,7 +353,7 @@ export class HuiPowerSourcesGraphCard
});
}
private _processData(stats: StatisticValue[][], trackY: (v: number) => void) {
private _processData(stats: StatisticValue[][]) {
const data: Record<number, number[]> = {};
stats.forEach((statSet) => {
statSet.forEach((point) => {
@@ -384,12 +369,8 @@ export class HuiPowerSourcesGraphCard
Object.entries(data).forEach(([x, y]) => {
const ts = Number(x);
const sumY = y.reduce((a, b) => a + b, 0);
const pos = Math.max(0, sumY);
const neg = Math.min(0, sumY);
positive.push([ts, pos]);
negative.push([ts, neg]);
trackY(pos);
trackY(neg);
positive.push([ts, Math.max(0, sumY)]);
negative.push([ts, Math.min(0, sumY)]);
});
return { positive, negative };
}
+10 -11
View File
@@ -1,6 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { customElement, query, queryAll, state } from "lit/decorators";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeDomain } from "../../../common/entity/compute_domain";
@@ -75,6 +75,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
@state() private _config?: EntitiesCardConfig;
@queryAll("#states > div > *") private _rowElements!: NodeListOf<HTMLElement>;
@query("hui-entities-toggle") private _entitiesToggle?: HTMLElement;
private _hass?: HomeAssistant;
private _configEntities?: LovelaceRowConfig[];
@@ -102,22 +106,17 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
set hass(hass: HomeAssistant) {
this._hass = hass;
this.shadowRoot
?.querySelectorAll("#states > div > *")
.forEach((element: unknown) => {
(element as LovelaceRow).hass = hass;
});
this._rowElements.forEach((element: unknown) => {
(element as LovelaceRow).hass = hass;
});
if (this._headerElement) {
this._headerElement.hass = hass;
}
if (this._footerElement) {
this._footerElement.hass = hass;
}
const entitiesToggle = this.shadowRoot?.querySelector(
"hui-entities-toggle"
);
if (entitiesToggle) {
(entitiesToggle as any).hass = hass;
if (this._entitiesToggle) {
(this._entitiesToggle as any).hass = hass;
}
}
@@ -225,7 +225,11 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (changedProps.has("hass") || changedProps.has("_config")) {
if (
changedProps.has("hass") ||
changedProps.has("_config") ||
changedProps.has("_metadata")
) {
this._computeNames();
}
@@ -112,6 +112,8 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
@query("ha-input", true) private _input!: HaInput;
@query("ha-list") private _list?: List;
private _unsubItems?: Promise<UnsubscribeFunc>;
private _refreshTimer?: number;
@@ -764,7 +766,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
let focusedIndex: number | undefined;
let list: List | undefined;
if (ev.type === "keydown") {
list = this.renderRoot.querySelector("ha-list")!;
list = this._list!;
focusedIndex = list.getFocusedItemIndex();
}
const item = this._getItem(ev.currentTarget.itemId);
@@ -894,7 +896,7 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
private async _moveItem(oldIndex: number, newIndex: number) {
await this.updateComplete;
const list = this.renderRoot.querySelector("ha-list")!;
const list = this._list!;
const items = list.children;
@@ -217,7 +217,6 @@ export class HuiActionEditor extends LitElement {
<ha-service-control
.hass=${this.hass}
.value=${this._serviceAction(this.config)}
.showAdvanced=${this.hass.userData?.showAdvanced}
narrow
@value-changed=${this._serviceValueChanged}
></ha-service-control>
@@ -2,7 +2,7 @@ import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
@@ -62,14 +62,15 @@ export class HuiCardPicker extends LitElement {
@state() private _filter = "";
@query("ha-input-search") private _searchInput?: HTMLElement;
private _unusedEntities?: string[];
private _usedEntities?: string[];
public async focus(): Promise<void> {
const searchInput = this.renderRoot.querySelector("ha-input-search");
if (searchInput) {
searchInput.focus();
if (this._searchInput) {
this._searchInput.focus();
} else {
await this.updateComplete;
this.focus();
@@ -65,7 +65,6 @@ export class HuiServiceButtonElementEditor
<ha-service-control
.hass=${this.hass}
.value=${this._serviceData(this._config)}
.showAdvanced=${this.hass.userData?.showAdvanced}
narrow
@value-changed=${this._serviceDataChanged}
></ha-service-control>
@@ -96,7 +96,6 @@ export class HuiButtonCardFeatureEditor
hide-description
.hass=${this.hass}
.value=${scriptData}
.showAdvanced=${this.hass.userData?.showAdvanced}
.narrow=${false}
@value-changed=${this._scriptFieldVariablesChanged}
></ha-service-control
@@ -152,7 +152,9 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
"lawn-mower-commands",
"media-player-playback",
"light-color-favorites",
"media-player-source",
"media-player-volume-buttons",
"media-player-volume-slider",
"numeric-input",
"select-options",
"trend-graph",
@@ -0,0 +1,99 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import type { MediaPlayerEntity } from "../../../../data/media-player";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type {
LovelaceCardFeatureContext,
MediaPlayerSourceCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
@customElement("hui-media-player-source-card-feature-editor")
export class HuiMediaPlayerSourceCardFeatureEditor
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerSourceCardFeatureConfig;
public setConfig(config: MediaPlayerSourceCardFeatureConfig): void {
this._config = config;
}
private _schema = memoizeOne(
(stateObj?: MediaPlayerEntity) =>
[
{
name: "sources",
selector: {
select: {
multiple: true,
mode: "list" as const,
reorder: true,
options:
stateObj?.attributes.source_list?.map((source) => ({
value: source,
label: source,
})) ?? [],
},
},
},
] as const
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const stateObj = this.context?.entity_id
? (this.hass.states[this.context.entity_id] as
| MediaPlayerEntity
| undefined)
: undefined;
const schema = this._schema(stateObj);
return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(
ev: ValueChangedEvent<MediaPlayerSourceCardFeatureConfig>
): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "sources":
return this.hass?.localize(
`ui.panel.lovelace.editor.features.types.media-player-source.${schema.name}`
);
default:
return "";
}
};
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-source-card-feature-editor": HuiMediaPlayerSourceCardFeatureEditor;
}
}
@@ -0,0 +1,90 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import { MediaPlayerEntityFeature } from "../../../../data/media-player";
import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import type {
LovelaceCardFeatureContext,
MediaPlayerVolumeSliderCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
@customElement("hui-media-player-volume-slider-card-feature-editor")
export class HuiMediaPlayerVolumeSliderCardFeatureEditor
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: MediaPlayerVolumeSliderCardFeatureConfig;
public setConfig(config: MediaPlayerVolumeSliderCardFeatureConfig): void {
this._config = config;
}
private _schema = memoizeOne(
(supportsMute: boolean) =>
[
{
name: "show_mute_button",
disabled: !supportsMute,
selector: { boolean: {} },
},
] as const
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const stateObj = this.context?.entity_id
? this.hass.states[this.context.entity_id]
: undefined;
const supportsMute =
!!stateObj &&
supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_MUTE);
const data: MediaPlayerVolumeSliderCardFeatureConfig = {
type: "media-player-volume-slider",
show_mute_button: this._config.show_mute_button ?? true,
};
const schema = this._schema(supportsMute);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(
ev: ValueChangedEvent<MediaPlayerVolumeSliderCardFeatureConfig>
): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass!.localize(
`ui.panel.lovelace.editor.features.types.media-player-volume-slider.${schema.name}`
);
}
declare global {
interface HTMLElementTagNameMap {
"hui-media-player-volume-slider-card-feature-editor": HuiMediaPlayerVolumeSliderCardFeatureEditor;
}
}
+13 -9
View File
@@ -1,7 +1,7 @@
import { mdiArrowLeft, mdiArrowRight, mdiPlus } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import type { LovelaceViewElement } from "../../../data/lovelace";
@@ -30,6 +30,12 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
@state() private _config?: LovelaceViewConfig;
@query("#main") private _oldMain?: HTMLElement;
@query("#sidebar") private _oldSidebar?: HTMLElement;
@query(".container") private _container?: HTMLElement;
private _mqlListenerRef?: () => void;
private _mql?: MediaQueryList;
@@ -123,20 +129,18 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
}
if (this.hasUpdated) {
const oldMain = this.renderRoot.querySelector("#main");
const oldSidebar = this.renderRoot.querySelector("#sidebar");
const container = this.renderRoot.querySelector(".container")!;
if (oldMain) {
container.removeChild(oldMain);
const container = this._container!;
if (this._oldMain) {
container.removeChild(this._oldMain);
}
if (oldSidebar) {
container.removeChild(oldSidebar);
if (this._oldSidebar) {
container.removeChild(this._oldSidebar);
}
container.appendChild(mainDiv);
container.appendChild(sidebarDiv);
} else {
this.updateComplete.then(() => {
const container = this.renderRoot.querySelector(".container")!;
const container = this._container!;
container.appendChild(mainDiv);
container.appendChild(sidebarDiv);
});
@@ -1,6 +1,6 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-dialog";
import "../../components/ha-hls-player";
@@ -16,6 +16,8 @@ export class HuiDialogWebBrowserPlayMedia extends LitElement {
@state() private _open = false;
@query("img") private _img?: HTMLImageElement;
public showDialog(params: WebBrowserPlayMediaDialogParams): void {
this._params = params;
this._open = true;
@@ -26,10 +28,9 @@ export class HuiDialogWebBrowserPlayMedia extends LitElement {
}
private _dialogClosed(): void {
const img = this.renderRoot.querySelector("img");
if (img) {
if (this._img) {
// Unload streaming images so the connection can be closed
img.src = "";
this._img.src = "";
}
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
+6 -2
View File
@@ -259,6 +259,8 @@
"volume_up": "Volume up",
"volume_down": "Volume down",
"volume_mute": "Mute",
"shuffle": "Shuffle",
"repeat": "Repeat",
"media_volume_up": "Volume up",
"media_volume_down": "Volume down",
"media_volume_mute": "Volume mute",
@@ -9992,7 +9994,8 @@
"label": "Media player sound mode"
},
"media-player-source": {
"label": "Media player source"
"label": "Media player source",
"sources": "Sources"
},
"media-player-volume-buttons": {
"label": "Media player volume buttons",
@@ -10000,7 +10003,8 @@
"show_mute_button": "Show mute button"
},
"media-player-volume-slider": {
"label": "Media player volume slider"
"label": "Media player volume slider",
"show_mute_button": "Show mute button"
},
"vacuum-commands": {
"label": "Vacuum commands",

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