mirror of
https://github.com/home-assistant/frontend.git
synced 2026-11-01 18:45:10 +00:00
Compare commits
1
Commits
dev
..
mig-list-9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2baec01d8a |
No files matched your search
@@ -51,8 +51,8 @@
|
||||
|
||||
- This PR fixes or closes issue: fixes #
|
||||
- This PR is related to issue or discussion:
|
||||
- Link to [documentation][docs-repository] pull request:
|
||||
- Link to [contributors documentation][contributors-repository] pull request:
|
||||
- Link to documentation pull request:
|
||||
- Link to developer documentation pull request:
|
||||
- Link to backend pull request:
|
||||
|
||||
## Checklist
|
||||
@@ -63,8 +63,7 @@
|
||||
for before merging your code.
|
||||
|
||||
AI tools are welcome, but contributors are responsible for *fully*
|
||||
understanding the code before submitting a PR. Please follow our AI policy:
|
||||
https://contributors.home-assistant.io/start/ai-policy/
|
||||
understanding the code before submitting a PR.
|
||||
-->
|
||||
|
||||
- [ ] I understand the code I am submitting and can explain how it works.
|
||||
@@ -106,5 +105,4 @@ To help with the load of incoming pull requests:
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[contributors-repository]: https://github.com/home-assistant/contributors.home-assistant.io
|
||||
[perfect-pr]: https://contributors.home-assistant.io/start/pull-requests/#before-you-open-it
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ActionDetail } from "@material/mwc-list/mwc-list";
|
||||
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
|
||||
import type { Auth, Connection } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
@@ -214,7 +215,7 @@ class HcCast extends LitElement {
|
||||
this.castManager.requestSession();
|
||||
}
|
||||
|
||||
private async _handlePickView(ev: CustomEvent<{ index: number }>) {
|
||||
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
|
||||
const path = this.lovelaceViews?.[ev.detail.index]?.path ?? ev.detail.index;
|
||||
await ensureConnectedCastSession(this.castManager!, this.auth!);
|
||||
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
|
||||
|
||||
@@ -50,18 +50,6 @@ With no cursor yet, <kbd>↓</kbd> and <kbd>Enter</kbd> start from the `value`
|
||||
row, or from the top match while searching. With one item in the list,
|
||||
<kbd>Enter</kbd> picks it whether or not the cursor has moved.
|
||||
|
||||
## Screen readers
|
||||
|
||||
DOM focus stays in the search field, so screen readers do not hear the list
|
||||
change. While searching, a visually hidden `role="status"` message says how
|
||||
many items match the search and names it, so a new search with the same count
|
||||
is still read out. It is cleared as soon as the list changes and filled in once
|
||||
typing pauses for half a second, so it doesn't talk over each typed character
|
||||
or describe the previous search. When nothing matches, it reads a not
|
||||
found label that takes the search, or says there are no results for it. Section
|
||||
titles, placeholder rows, "Add custom item", the additional items and rows that
|
||||
`isSearchResult` rejects are not counted.
|
||||
|
||||
## Implementation
|
||||
|
||||
Without `sections`, the component filters the items from `getItems` itself as the
|
||||
@@ -91,7 +79,6 @@ means supplying one.
|
||||
| allowCustomValue | Boolean | false | Offers the typed string as an item. |
|
||||
| customValueLabel | String | - | Label for that custom item. |
|
||||
| notFoundLabel | String/Function | - | Shown when a search matches nothing. |
|
||||
| isSearchResult | Function | - | Whether a row from `getItems` counts as a match for screen readers. The quick bar leaves out its "Ask Assist" row. |
|
||||
| emptyLabel | String | - | Shown when there are no items at all. |
|
||||
| noSort | Boolean | false | Keeps the order `getItems` returned. |
|
||||
| clearable | Boolean | false | Adjusts search field padding for a clear affordance. |
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { IntlMessageFormat } from "intl-messageformat";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../../../../src/common/translations/localize";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-picker-combo-box";
|
||||
import type { PickerComboBoxItem } from "../../../../src/components/ha-picker-combo-box";
|
||||
@@ -21,26 +19,13 @@ import type {
|
||||
} from "../../../../src/types";
|
||||
import { THEME_COMPARISON_PANELS } from "../../components/demo-theme-comparison";
|
||||
|
||||
// The English strings, plurals included, so searching a demo announces the
|
||||
// same count and search as the real picker.
|
||||
const LOCALIZE_KEYS: Record<string, string> = {
|
||||
"ui.common.search": "Search",
|
||||
"ui.components.combo-box.no_match": "No matching items found",
|
||||
"ui.components.combo-box.no_items": "No items available",
|
||||
"ui.components.combo-box.result_count":
|
||||
"{count} {count, plural,\n one {result}\n other {results}\n} for {term}",
|
||||
"ui.components.combo-box.no_results": "No results for {term}",
|
||||
};
|
||||
|
||||
const localize: LocalizeFunc = (key, values) => {
|
||||
const message = LOCALIZE_KEYS[key];
|
||||
if (message === undefined) {
|
||||
return key;
|
||||
}
|
||||
return new IntlMessageFormat(message, "en").format<string>(
|
||||
values as Record<string, string | number>
|
||||
) as string;
|
||||
};
|
||||
const localize = (key: string) => LOCALIZE_KEYS[key] ?? key;
|
||||
|
||||
const DEMO_I18N: HomeAssistantInternationalization = {
|
||||
localize,
|
||||
|
||||
+10
-10
@@ -82,7 +82,7 @@
|
||||
"@tsparticles/preset-links": "4.5.0",
|
||||
"@versatiles/style": "7.0.1",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vvo/tzdb": "6.202.0",
|
||||
"@vvo/tzdb": "6.201.1",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"barcode-detector": "3.2.3",
|
||||
"cally": "0.9.2",
|
||||
@@ -100,8 +100,8 @@
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fullcalendar": "7.1.3",
|
||||
"fuse.js": "7.5.0",
|
||||
"hls.js": "1.7.4",
|
||||
"home-assistant-js-websocket": "9.8.1",
|
||||
"hls.js": "1.7.3",
|
||||
"home-assistant-js-websocket": "9.8.0",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "12.1.4",
|
||||
"js-yaml": "5.5.0",
|
||||
@@ -110,7 +110,7 @@
|
||||
"lit": "3.3.3",
|
||||
"lit-html": "3.3.3",
|
||||
"maplibre-gl": "6.16.0",
|
||||
"marked": "18.1.2",
|
||||
"marked": "18.1.1",
|
||||
"memoize-one": "6.0.0",
|
||||
"nanoid": "6.0.2",
|
||||
"node-vibrant": "4.0.4",
|
||||
@@ -153,7 +153,7 @@
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@playwright/test": "1.64.1",
|
||||
"@rsdoctor/rspack-plugin": "1.6.5",
|
||||
"@rspack/core": "2.3.0",
|
||||
"@rspack/core": "2.2.9",
|
||||
"@rspack/dev-server": "2.2.2",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "7.0.1",
|
||||
@@ -173,7 +173,7 @@
|
||||
"@vitest/coverage-v8": "5.0.5",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-polyfill-corejs3": "1.0.0",
|
||||
"browserslist": "4.29.5",
|
||||
"browserslist": "4.29.4",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"core-js-compat": "3.51.0",
|
||||
"del": "9.0.1",
|
||||
@@ -188,7 +188,7 @@
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.4.1",
|
||||
"generate-license-file": "4.2.5",
|
||||
"glob": "14.0.3",
|
||||
"glob": "13.0.6",
|
||||
"globals": "17.13.0",
|
||||
"gulp": "5.0.1",
|
||||
"gulp-json-transform": "0.5.0",
|
||||
@@ -214,7 +214,7 @@
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.72.1",
|
||||
"typescript-eslint": "8.72.0",
|
||||
"vite": "8.3.5",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "5.0.5",
|
||||
@@ -222,8 +222,8 @@
|
||||
"webpackbar": "7.0.0",
|
||||
"workbox-build": "7.4.1"
|
||||
},
|
||||
"packageManager": "[email protected]6.1",
|
||||
"packageManager": "[email protected]5.0",
|
||||
"volta": {
|
||||
"node": "24.22.2"
|
||||
"node": "24.22.1"
|
||||
}
|
||||
}
|
||||
Generated
+293
-326
File diff suppressed because it is too large.
Load diff
@@ -4,8 +4,8 @@ import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-icon-next";
|
||||
import "../components/item/ha-list-item-button";
|
||||
import "../components/list/ha-list-base";
|
||||
import "../components/ha-list";
|
||||
import "../components/ha-list-item";
|
||||
import type { AuthProvider } from "../data/auth";
|
||||
|
||||
declare global {
|
||||
@@ -32,19 +32,21 @@ export class HaPickAuthProvider extends LitElement {
|
||||
>${this._localize("ui.panel.page-authorize.pick_auth_provider")}</span
|
||||
>
|
||||
</h3>
|
||||
<ha-list-base>
|
||||
<ha-list>
|
||||
${this.authProviders.map(
|
||||
(provider) => html`
|
||||
<ha-list-item-button
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
role="button"
|
||||
.auth_provider=${provider}
|
||||
@click=${this._handlePick}
|
||||
>
|
||||
<span slot="headline">${provider.name}</span>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
${provider.name}
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list-base>
|
||||
</ha-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -78,9 +80,9 @@ export class HaPickAuthProvider extends LitElement {
|
||||
background: var(--card-background-color);
|
||||
padding: 0 15px;
|
||||
}
|
||||
ha-list-base {
|
||||
ha-list {
|
||||
margin: 16px -16px 0;
|
||||
--ha-row-item-padding-inline: 24px;
|
||||
--mdc-list-side-padding: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ const RASTER_TILE_URL = `${MAP_TILES_PATH}/raster/{z}/{x}/{y}.png?token={token}`
|
||||
// policy strips again unless the tiles ask for it back.
|
||||
const DEMO_RASTER_TILE_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||||
const OSM_ATTRIBUTION =
|
||||
'<a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>';
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
|
||||
|
||||
// Browsers keep about 16 live WebGL contexts and drop the oldest, which a
|
||||
// dashboard full of map cards hits. A transient loss is restored, hence a grace.
|
||||
|
||||
@@ -89,8 +89,6 @@ export const DEFAULT_CHART_WIDTH = 500;
|
||||
// is short enough for a whole screenful; on a desktop that would cover the page.
|
||||
const VISIBILITY_ROOT_MARGIN_NARROW = "100%";
|
||||
const VISIBILITY_ROOT_MARGIN = "300px";
|
||||
|
||||
export type ZoomSliderMode = "auto" | "always" | "never";
|
||||
const DEFERRED_PROPS = [
|
||||
"options",
|
||||
"data",
|
||||
@@ -165,10 +163,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
@property({ attribute: "small-controls", type: Boolean })
|
||||
public smallControls?: boolean;
|
||||
|
||||
// "auto" shows the zoom slider once zoomed. "never" hides every zoom
|
||||
// control, for charts zoomed from outside like the synced History charts.
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
// For charts zoomed from outside, like the synced History charts
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
|
||||
// For charts sized to their content, like timelines, which grow to fit the
|
||||
// zoom slider instead of giving up plot height
|
||||
@@ -620,7 +617,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
>
|
||||
${
|
||||
(this._isZoomed || this._isGraphRoamed) &&
|
||||
this.zoomSlider !== "never" &&
|
||||
!this.hideZoomControls &&
|
||||
!showZoomSlider
|
||||
? html`<ha-icon-button
|
||||
class="zoom-reset"
|
||||
@@ -1067,12 +1064,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
private _showZoomSlider() {
|
||||
const [start, end] = this._zoomRange;
|
||||
return (
|
||||
this.zoomSlider !== "never" &&
|
||||
!this.hideZoomControls &&
|
||||
this._hasZoomableXAxis() &&
|
||||
(this.zoomSlider === "always" ||
|
||||
this._zoomSliderHeld ||
|
||||
start !== 0 ||
|
||||
end !== 100)
|
||||
(this._zoomSliderHeld || start !== 0 || end !== 100)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2312,9 +2306,6 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
--ha-assist-chip-trailing-space: var(--ha-space-2);
|
||||
--ha-assist-chip-icon-label-space: var(--ha-space-1);
|
||||
}
|
||||
.zoom-reset {
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -648,7 +648,7 @@ export class HaNetworkGraph extends LitElement {
|
||||
|
||||
ha-icon-button,
|
||||
::slotted(ha-icon-button) {
|
||||
margin-inline-end: var(--ha-space-3);
|
||||
margin-right: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import type {
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { ZoomSliderMode } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import {
|
||||
@@ -106,8 +105,8 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
|
||||
@state() private _chartData: LineSeriesOption[] = [];
|
||||
|
||||
@@ -160,7 +159,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.expandLegend=${this.expandLegend}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
.hideZoomControls=${this.hideZoomControls}
|
||||
.clickLabelForMoreInfo=${this.clickForMoreInfo}
|
||||
@legend-label-click=${this._handleLegendLabelClick}
|
||||
></ha-chart-base>
|
||||
|
||||
@@ -22,7 +22,6 @@ import type {
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import { DEFAULT_CHART_WIDTH, MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import type { ZoomSliderMode } from "./ha-chart-base";
|
||||
import { itemTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import type { HaECOption, HaECSeries } from "../../resources/echarts/echarts";
|
||||
@@ -69,8 +68,8 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public chartIndex?;
|
||||
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
@property({ attribute: "hide-zoom-controls", type: Boolean })
|
||||
public hideZoomControls = false;
|
||||
|
||||
@state() private _chartData: CustomSeriesOption[] = [];
|
||||
|
||||
@@ -106,7 +105,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
grow-for-zoom-slider
|
||||
@chart-click=${this._handleChartClick}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
.hideZoomControls=${this.hideZoomControls}
|
||||
></ha-chart-base>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
.height=${this.virtualize ? undefined : this.height}
|
||||
.expandLegend=${this.expandLegend}
|
||||
.zoomSlider=${this.syncCharts ? "never" : "auto"}
|
||||
?hide-zoom-controls=${this.syncCharts}
|
||||
></state-history-chart-line>
|
||||
</div> `;
|
||||
}
|
||||
@@ -237,7 +237,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.clickForMoreInfo=${this.clickForMoreInfo}
|
||||
@y-width-changed=${this._yWidthChanged}
|
||||
@chart-zoom-with-index=${this._handleTimelineSync}
|
||||
.zoomSlider=${this.syncCharts ? "never" : "auto"}
|
||||
?hide-zoom-controls=${this.syncCharts}
|
||||
></state-history-chart-timeline>
|
||||
</div> `;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TZDate } from "@date-fns/tz";
|
||||
import type { ContextType } from "@lit/context";
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiCalendarToday } from "@mdi/js";
|
||||
import "cally";
|
||||
import type { HassConfig } from "home-assistant-js-websocket/dist/types";
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
HASSDomTargetEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
@@ -32,8 +32,8 @@ import type { HaBaseTimeInput } from "../ha-base-time-input";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-icon-button-next";
|
||||
import "../ha-icon-button-prev";
|
||||
import "../item/ha-list-item-button";
|
||||
import "../list/ha-list-base";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
import type { DateRangePickerRanges } from "./ha-date-range-picker";
|
||||
@@ -166,19 +166,11 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-list-base>
|
||||
${Object.entries(this.ranges!).map(
|
||||
([name, range], index) => html`
|
||||
<ha-list-item-button
|
||||
.range=${range}
|
||||
.index=${index}
|
||||
@click=${this._setDateRange}
|
||||
>
|
||||
<span slot="headline">${name}</span>
|
||||
</ha-list-item-button>
|
||||
`
|
||||
<ha-list @action=${this._setDateRange} activatable>
|
||||
${Object.keys(this.ranges!).map(
|
||||
(name) => html`<ha-list-item>${name}</ha-list-item>`
|
||||
)}
|
||||
</ha-list-base>
|
||||
</ha-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -351,12 +343,11 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
this._saveDateRangePreset(chip.range, chip.index);
|
||||
}
|
||||
|
||||
private _setDateRange(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HTMLElement & { range: [Date, Date]; index: number }
|
||||
>
|
||||
) {
|
||||
this._saveDateRangePreset(ev.currentTarget.range, ev.currentTarget.index);
|
||||
private _setDateRange(ev: HASSDomEvent<ActionDetail>) {
|
||||
const dateRange: [Date, Date] = Object.values(this.ranges!)[
|
||||
ev.detail.index
|
||||
];
|
||||
this._saveDateRangePreset(dateRange, ev.detail.index);
|
||||
}
|
||||
|
||||
private _saveDateRangePreset(range: [Date, Date], index: number) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { tinykeys } from "tinykeys";
|
||||
import { findSectionTitle } from "./list/find-section-title";
|
||||
import { searchResultsStatus } from "./list/search-results-status";
|
||||
import { consume } from "../common/decorators/consume";
|
||||
import {
|
||||
fireEvent,
|
||||
@@ -24,7 +23,6 @@ import {
|
||||
} from "../common/dom/fire_event";
|
||||
import { ignoreRepeatedActivation } from "../common/keyboard/ignore-repeated-activation";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import {
|
||||
@@ -166,10 +164,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@property({ attribute: false })
|
||||
public notFoundLabel?: string | ((search: string) => string);
|
||||
|
||||
/** Whether a row from `getItems` counts as a search result for screen readers, so a row offered for any search, like the quick bar's "Ask Assist", can be left out */
|
||||
@property({ attribute: false })
|
||||
public isSearchResult?: (item: PickerComboBoxItem) => boolean;
|
||||
|
||||
@property({ attribute: "empty-label" })
|
||||
public emptyLabel?: string;
|
||||
|
||||
@@ -230,18 +224,12 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@state() private _sectionTitle?: string;
|
||||
|
||||
@state() private _status?: string;
|
||||
|
||||
private _allItems: PickerComboBoxItem[] = [];
|
||||
|
||||
// Rows from this index on are added by the combo box itself, like "Add custom
|
||||
// item", and belong to no section.
|
||||
private _sectionedItemsCount = 0;
|
||||
|
||||
// Rows the combo box offers for any search, like "Add custom item" and the
|
||||
// additional items, are not search results.
|
||||
private _actionItems = new WeakSet<PickerComboBoxItem>();
|
||||
|
||||
// The virtualized list renders its scroller a moment after it connects, so
|
||||
// the scroll fades attach once it reports its first visible rows.
|
||||
private _virtualScrollElement?: HTMLElement;
|
||||
@@ -263,18 +251,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
public willUpdate() {
|
||||
if (!this.hasUpdated) {
|
||||
this._selectedSection = this.selectedSection;
|
||||
this._allItems = this._getItems();
|
||||
this._items = this._allItems;
|
||||
this._updateListMode();
|
||||
}
|
||||
// New rows make the last message out of date, so clear it at once. The new
|
||||
// one is then read out even when its text is the same as before.
|
||||
if (changedProps.has("_items")) {
|
||||
this._status = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
@@ -284,13 +267,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
if (changedProps.has("_items") && this._search) {
|
||||
this._highlightTopMatch();
|
||||
}
|
||||
if (changedProps.has("_items")) {
|
||||
if (this._search) {
|
||||
this._updateStatus();
|
||||
} else {
|
||||
this._updateStatus.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll before the surface animates in, so the list does not jump at the
|
||||
@@ -327,8 +303,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._removeKeyboardShortcuts?.();
|
||||
this._updateStatus.cancel();
|
||||
this._status = undefined;
|
||||
}
|
||||
|
||||
public refreshItems() {
|
||||
@@ -388,28 +362,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
>
|
||||
${this._plainList ? this._renderPlainList() : this._renderVirtualList()}
|
||||
${this.renderScrollableFades()}
|
||||
</div>
|
||||
<div class="visually-hidden" role="status">
|
||||
${this._status ?? nothing}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// DOM focus stays in the search field, so screen readers hear nothing when
|
||||
// the list changes. Announce how many items match instead, once typing
|
||||
// pauses, so the count doesn't compete with each typed character.
|
||||
private _updateStatus = debounce(() => {
|
||||
this._status = searchResultsStatus(
|
||||
this._search,
|
||||
this._items,
|
||||
(item) =>
|
||||
this._isOption(item) &&
|
||||
!this._actionItems.has(item) &&
|
||||
(this.isSearchResult?.(item) ?? true),
|
||||
this.i18n?.localize,
|
||||
this.notFoundLabel
|
||||
);
|
||||
}, 500);
|
||||
|
||||
private _renderPlainList() {
|
||||
return html`
|
||||
<ha-list-selectable
|
||||
@@ -499,25 +454,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
}
|
||||
|
||||
private _getAdditionalItems = (searchString?: string) => {
|
||||
const items = this.getAdditionalItems?.(searchString) || [];
|
||||
items.forEach((item) => this._actionItems.add(item));
|
||||
return items;
|
||||
};
|
||||
|
||||
private _getCustomValueItem(search: string): PickerComboBoxItem {
|
||||
const item = {
|
||||
id: search,
|
||||
primary:
|
||||
this.customValueLabel ??
|
||||
this.i18n?.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
"Add custom item",
|
||||
secondary: `"${search}"`,
|
||||
icon_path: mdiPlus,
|
||||
};
|
||||
this._actionItems.add(item);
|
||||
return item;
|
||||
}
|
||||
private _getAdditionalItems = (searchString?: string) =>
|
||||
this.getAdditionalItems?.(searchString) || [];
|
||||
|
||||
private _getItems = () => {
|
||||
let items = [...(this.getItems(this._search, this._selectedSection) || [])];
|
||||
@@ -559,7 +497,15 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
items.push(...additionalItems);
|
||||
|
||||
if (this.allowCustomValue && this._search) {
|
||||
items.push(this._getCustomValueItem(this._search));
|
||||
items.push({
|
||||
id: this._search,
|
||||
primary:
|
||||
this.customValueLabel ??
|
||||
this.i18n?.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
"Add custom item",
|
||||
secondary: `"${this._search}"`,
|
||||
icon_path: mdiPlus,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.mode === "dialog") {
|
||||
@@ -720,7 +666,15 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
if (this.allowCustomValue && searchString) {
|
||||
filteredItems.push(this._getCustomValueItem(searchString));
|
||||
filteredItems.push({
|
||||
id: searchString,
|
||||
primary:
|
||||
this.customValueLabel ??
|
||||
this.i18n?.localize?.("ui.components.combo-box.add_custom_item") ??
|
||||
"Add custom item",
|
||||
secondary: `"${searchString}"`,
|
||||
icon_path: mdiPlus,
|
||||
});
|
||||
}
|
||||
|
||||
this._items = filteredItems;
|
||||
@@ -1064,17 +1018,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
align-items: center;
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { css } from "lit";
|
||||
import { RadioListItemBase } from "@material/mwc-list/mwc-radio-list-item-base";
|
||||
import { styles as controlStyles } from "@material/mwc-list/mwc-control-list-item.css";
|
||||
import { styles } from "@material/mwc-list/mwc-list-item.css";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
@customElement("ha-radio-list-item")
|
||||
export class HaRadioListItem extends RadioListItemBase {
|
||||
async onChange(event) {
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
styles,
|
||||
controlStyles,
|
||||
css`
|
||||
:host {
|
||||
--mdc-theme-secondary: var(--primary-color);
|
||||
}
|
||||
|
||||
:host([graphic="avatar"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="medium"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="large"]) .mdc-deprecated-list-item__graphic,
|
||||
:host([graphic="control"]) .mdc-deprecated-list-item__graphic {
|
||||
margin-inline-end: var(--mdc-list-item-graphic-margin, 16px);
|
||||
margin-inline-start: 0px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
.mdc-deprecated-list-item__meta {
|
||||
flex-shrink: 0;
|
||||
direction: var(--direction);
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
.mdc-deprecated-list-item__graphic {
|
||||
margin-top: var(--radio-list-item-graphic-margin-top);
|
||||
}
|
||||
:host([graphic="icon"]) .mdc-deprecated-list-item__graphic {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: var(--mdc-list-item-graphic-margin, 32px);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-radio-list-item": HaRadioListItem;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
/**
|
||||
* The screen reader message for a list search: how many items match, or that
|
||||
* nothing does. A live region is only read out when its text changes, so the
|
||||
* message names the search.
|
||||
*
|
||||
* Returns undefined when there is no search.
|
||||
* @param search - The search text
|
||||
* @param items - The list rows, with section titles as strings
|
||||
* @param isResult - Whether a row is a result, so placeholder rows and rows
|
||||
* offered for any search, like "Add custom item", are not counted
|
||||
* @param localize - The localize function
|
||||
* @param notFoundLabel - The not found label; one that takes the search is
|
||||
* used when nothing matches, as it names the search already
|
||||
*/
|
||||
export const searchResultsStatus = <T>(
|
||||
search: string,
|
||||
items: readonly (T | string)[],
|
||||
isResult: (item: T) => boolean,
|
||||
localize?: LocalizeFunc,
|
||||
notFoundLabel?: string | ((search: string) => string)
|
||||
): string | undefined => {
|
||||
if (!search) {
|
||||
return undefined;
|
||||
}
|
||||
const count = items.filter(
|
||||
(item): item is T => typeof item !== "string" && isResult(item)
|
||||
).length;
|
||||
const term = `‘${search}’`;
|
||||
if (!count) {
|
||||
return typeof notFoundLabel === "function"
|
||||
? notFoundLabel(search)
|
||||
: localize?.("ui.components.combo-box.no_results", { term });
|
||||
}
|
||||
return localize?.("ui.components.combo-box.result_count", { count, term });
|
||||
};
|
||||
@@ -282,7 +282,6 @@ export class QuickBar extends LitElement {
|
||||
)}
|
||||
.label=${this.hass.localize("ui.dialogs.quick-bar.title")}
|
||||
.getItems=${this._getItems}
|
||||
.isSearchResult=${this._isSearchResult}
|
||||
.rowRenderer=${this._getRowRenderer(this._runningCommandId)}
|
||||
mode="dialog"
|
||||
.sections=${sections}
|
||||
@@ -447,10 +446,6 @@ export class QuickBar extends LitElement {
|
||||
|
||||
// #region data
|
||||
|
||||
// "Ask Assist" is offered for any search, so it isn't a search result.
|
||||
private _isSearchResult = (item: PickerComboBoxItem) =>
|
||||
!("assistPrompt" in item);
|
||||
|
||||
private _getItems = (searchString: string, section: string) => {
|
||||
this._selectedSection = section as QuickBarSection | undefined;
|
||||
return this._getItemsMemoized(
|
||||
|
||||
@@ -180,13 +180,6 @@ export class HomeAssistantMain extends LitElement {
|
||||
color: var(--primary-text-color);
|
||||
/* remove the grey tap highlights in iOS on the fullscreen touch targets */
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
/* Keep the negative z-index panel backgrounds (the resolver's ::before
|
||||
and the Lovelace view background) in this host's stacking context.
|
||||
Isolating the conditionally rendered partial-panel-resolver instead
|
||||
can intermittently leave the Android WebView (Chromium 109-124)
|
||||
accessibility tree with an empty panel subtree while the page paints
|
||||
normally, which breaks UI automation such as the companion app E2E. */
|
||||
isolation: isolate;
|
||||
--ha-sidebar-width: calc(56px + var(--safe-area-inset-left, 0px));
|
||||
--ha-top-app-bar-width: calc(100% - var(--ha-sidebar-width));
|
||||
--safe-area-content-inset-left: 0px;
|
||||
@@ -208,6 +201,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
partial-panel-resolver {
|
||||
display: block;
|
||||
height: 100%;
|
||||
isolation: isolate;
|
||||
}
|
||||
partial-panel-resolver::before {
|
||||
content: "";
|
||||
|
||||
@@ -292,8 +292,8 @@ export class HaConfigAppsInstalled extends LitElement {
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--ha-space-6) var(--ha-space-4) var(--ha-space-4);
|
||||
margin: 0 0 var(--ha-space-18);
|
||||
padding: var(--ha-space-4);
|
||||
margin-bottom: var(--ha-space-18);
|
||||
gap: var(--ha-space-4);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(336px, 100%), 1fr));
|
||||
|
||||
@@ -347,22 +347,11 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
<span class="segment-label" title=${segment.label}
|
||||
>${segment.label}</span
|
||||
>
|
||||
<span class="segment-label">${segment.label}</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
${
|
||||
this.manager.stage
|
||||
? html`<span class="stage-label"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.backup.overview.progress.description.create_backup.${this.manager.stage}`
|
||||
)}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -483,7 +472,6 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
css`
|
||||
.progress-content {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
|
||||
container-type: inline-size;
|
||||
}
|
||||
.segmented-progress {
|
||||
display: flex;
|
||||
@@ -529,21 +517,6 @@ export class HaBackupOverviewProgress extends LitElement {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.stage-label {
|
||||
display: none;
|
||||
margin-top: var(--ha-space-1);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
/* Segment labels don't fit a narrow bar, name the current stage instead */
|
||||
@container (max-width: 479px) {
|
||||
.segment-label {
|
||||
display: none;
|
||||
}
|
||||
.stage-label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
|
||||
@@ -357,19 +357,13 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
justify-content: flex-end;
|
||||
border-top: none;
|
||||
}
|
||||
ha-list-base {
|
||||
--ha-list-padding: 0 0 var(--ha-space-2);
|
||||
}
|
||||
ha-list-item-base {
|
||||
--ha-row-item-padding-block: var(--ha-space-2);
|
||||
--ha-row-item-min-height: 40px;
|
||||
--ha-row-item-min-height: 40x;
|
||||
}
|
||||
ha-list-item-base::part(start) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
ha-list-item-base::part(headline) {
|
||||
white-space: wrap;
|
||||
}
|
||||
span.skeleton {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
@@ -101,8 +101,6 @@ export class DialogYamlIntegration extends LitElement {
|
||||
ha-dialog {
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
--ha-dialog-header-white-space: normal;
|
||||
--ha-dialog-header-title-height: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,9 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/item/ha-list-item-base";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import "../../../components/user/ha-person-badge";
|
||||
import type { Person } from "../../../data/person";
|
||||
import {
|
||||
@@ -93,22 +92,23 @@ export class HaConfigPerson extends LitElement {
|
||||
</span>
|
||||
|
||||
<ha-card outlined class="storage">
|
||||
<ha-list-base>
|
||||
<ha-list>
|
||||
${this._storageItems.map(
|
||||
(entry) => html`
|
||||
<ha-list-item-button
|
||||
<ha-list-item
|
||||
graphic="avatar"
|
||||
@click=${this._openEditEntry}
|
||||
.entry=${entry}
|
||||
>
|
||||
<ha-person-badge
|
||||
.person=${entry}
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
></ha-person-badge>
|
||||
<span slot="headline">${entry.name}</span>
|
||||
</ha-list-item-button>
|
||||
<span>${entry.name}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list-base>
|
||||
</ha-list>
|
||||
${
|
||||
this._storageItems.length === 0
|
||||
? html`
|
||||
@@ -138,19 +138,19 @@ export class HaConfigPerson extends LitElement {
|
||||
this._configItems.length > 0
|
||||
? html`
|
||||
<ha-card outlined header="Configuration.yaml persons">
|
||||
<ha-list-base>
|
||||
<ha-list>
|
||||
${this._configItems.map(
|
||||
(entry) => html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item graphic="avatar">
|
||||
<ha-person-badge
|
||||
.person=${entry}
|
||||
slot="start"
|
||||
slot="graphic"
|
||||
></ha-person-badge>
|
||||
<span slot="headline">${entry.name}</span>
|
||||
</ha-list-item-base>
|
||||
<span>${entry.name}</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-list-base>
|
||||
</ha-list>
|
||||
</ha-card>
|
||||
`
|
||||
: nothing
|
||||
@@ -299,7 +299,7 @@ export class HaConfigPerson extends LitElement {
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
ha-list-base:has(+ .empty) {
|
||||
ha-list:has(+ .empty) {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -38,8 +38,10 @@ import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/item/ha-list-item-base";
|
||||
import "../../../components/item/ha-list-item-button";
|
||||
import "../../../components/list/ha-list-base";
|
||||
import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
|
||||
import {
|
||||
fireRelatedContext,
|
||||
@@ -441,7 +443,7 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
@click=${this._deleteDevice}
|
||||
></ha-icon-button>
|
||||
</h1>
|
||||
<ha-list>
|
||||
<ha-list-base>
|
||||
${device.entities.map((entityId) => {
|
||||
const entityStateObj = this.hass.states[entityId];
|
||||
if (!entityStateObj) {
|
||||
@@ -469,48 +471,46 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
entityId,
|
||||
entityStateObj
|
||||
);
|
||||
return html`
|
||||
<ha-list-item
|
||||
hasMeta
|
||||
?twoline=${!!secondary}
|
||||
graphic="icon"
|
||||
.entityId=${entityId}
|
||||
@click=${
|
||||
this._mode === "live"
|
||||
? this._showMoreInfo
|
||||
: undefined
|
||||
}
|
||||
.noninteractive=${this._mode === "review"}
|
||||
>
|
||||
${
|
||||
badgeStateObj
|
||||
? html`
|
||||
<state-badge
|
||||
.stateObj=${badgeStateObj}
|
||||
slot="graphic"
|
||||
></state-badge>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${primary}
|
||||
${
|
||||
secondary
|
||||
? html`<span slot="secondary"
|
||||
>${secondary}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
integrationName
|
||||
? html`<span slot="meta" class="domain"
|
||||
>${integrationName}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
</ha-list-item>
|
||||
const rowContent = html`
|
||||
${
|
||||
badgeStateObj
|
||||
? html`
|
||||
<state-badge
|
||||
.stateObj=${badgeStateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<span slot="headline">${primary}</span>
|
||||
${
|
||||
secondary
|
||||
? html`<span slot="supporting-text"
|
||||
>${secondary}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
integrationName
|
||||
? html`<span slot="end" class="domain"
|
||||
>${integrationName}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
return this._mode === "live"
|
||||
? html`<ha-list-item-button
|
||||
class="entity"
|
||||
.entityId=${entityId}
|
||||
@click=${this._showMoreInfo}
|
||||
>
|
||||
${rowContent}
|
||||
</ha-list-item-button>`
|
||||
: html`<ha-list-item-base class="entity">
|
||||
${rowContent}
|
||||
</ha-list-item-base>`;
|
||||
})}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
@@ -556,7 +556,7 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
entities.length
|
||||
? html`
|
||||
<ha-card outlined class="entities">
|
||||
<ha-list>
|
||||
<ha-list-base>
|
||||
${entities.map((entityId) => {
|
||||
const entityStateObj = this.hass.states[entityId];
|
||||
if (!entityStateObj) {
|
||||
@@ -583,53 +583,50 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
entityId,
|
||||
entityStateObj
|
||||
);
|
||||
return html`
|
||||
<ha-list-item
|
||||
class="entity"
|
||||
hasMeta
|
||||
?twoline=${!!secondary}
|
||||
graphic="icon"
|
||||
.entityId=${entityId}
|
||||
@click=${
|
||||
this._mode === "live"
|
||||
? this._showMoreInfo
|
||||
: undefined
|
||||
}
|
||||
.noninteractive=${this._mode === "review"}
|
||||
>
|
||||
${
|
||||
badgeStateObj
|
||||
? html`
|
||||
<state-badge
|
||||
.stateObj=${badgeStateObj}
|
||||
slot="graphic"
|
||||
></state-badge>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${primary}
|
||||
${
|
||||
secondary
|
||||
? html`<span slot="secondary"
|
||||
>${secondary}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
<div slot="meta">
|
||||
<span class="domain">${domainName}</span>
|
||||
<ha-icon-button
|
||||
.path=${mdiDelete}
|
||||
.entityId=${entityId}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.scene.editor.entities.delete"
|
||||
)}
|
||||
@click=${this._deleteEntity}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
</ha-list-item>
|
||||
const rowContent = html`
|
||||
${
|
||||
badgeStateObj
|
||||
? html`
|
||||
<state-badge
|
||||
.stateObj=${badgeStateObj}
|
||||
slot="start"
|
||||
></state-badge>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<span slot="headline">${primary}</span>
|
||||
${
|
||||
secondary
|
||||
? html`<span slot="supporting-text"
|
||||
>${secondary}</span
|
||||
>`
|
||||
: nothing
|
||||
}
|
||||
<div slot="end">
|
||||
<span class="domain">${domainName}</span>
|
||||
<ha-icon-button
|
||||
.path=${mdiDelete}
|
||||
.entityId=${entityId}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.scene.editor.entities.delete"
|
||||
)}
|
||||
@click=${this._deleteEntity}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`;
|
||||
return this._mode === "live"
|
||||
? html`<ha-list-item-button
|
||||
class="entity"
|
||||
.entityId=${entityId}
|
||||
@click=${this._showMoreInfo}
|
||||
>
|
||||
${rowContent}
|
||||
</ha-list-item-button>`
|
||||
: html`<ha-list-item-base class="entity">
|
||||
${rowContent}
|
||||
</ha-list-item-base>`;
|
||||
})}
|
||||
</ha-list>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
`
|
||||
: ""
|
||||
@@ -1510,19 +1507,17 @@ export class HaSceneEditor extends DirtyStateProviderMixin<number>()(
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
}
|
||||
div[slot="meta"] {
|
||||
div[slot="end"] {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
ha-list-item {
|
||||
ha-list-item-button,
|
||||
ha-list-item-base {
|
||||
/* let the trailing label size to its content instead of the default
|
||||
fixed meta width, which would clip it */
|
||||
--mdc-list-item-meta-size: auto;
|
||||
}
|
||||
ha-list-item.entity {
|
||||
padding-right: 28px;
|
||||
--ha-row-item-gap: var(--ha-space-2);
|
||||
}
|
||||
.domain {
|
||||
font-size: var(--ha-font-size-s);
|
||||
|
||||
@@ -73,10 +73,6 @@ const mountPartitionLabel = ({
|
||||
return drive ? `${drive} — ${identity}, ${size}` : `${identity}, ${size}`;
|
||||
};
|
||||
|
||||
// Supervisor accepts only ASCII letters, digits and underscores.
|
||||
const mountNameFromLabel = (label?: string): string =>
|
||||
(label ?? "").replace(/^\W+|\W+$/g, "").replace(/\W+/g, "_");
|
||||
|
||||
const requiredFieldsFilled = (
|
||||
schema: readonly HaFormSchema[],
|
||||
data?: MountFormData
|
||||
@@ -309,8 +305,6 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
|
||||
private _originalType?: SupervisorMountType;
|
||||
|
||||
private _suggestedName?: string;
|
||||
|
||||
@state() private _diskIdentity?: string;
|
||||
|
||||
public connectedCallback(): void {
|
||||
@@ -535,9 +529,7 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._validationError = {};
|
||||
this._validationWarning = {};
|
||||
const previous = this._data;
|
||||
this._data = ev.detail.value;
|
||||
this._suggestName(previous);
|
||||
if (this._data?.name && !/^\w+$/.test(this._data.name)) {
|
||||
this._validationError.name = "invalid_name";
|
||||
}
|
||||
@@ -564,32 +556,6 @@ class ViewMountDialog extends DirtyStateProviderMixin<
|
||||
);
|
||||
}
|
||||
|
||||
// Name a new disk mount after the partition's volume label, unless the user
|
||||
// typed a name of their own.
|
||||
private _suggestName(previous?: MountFormData) {
|
||||
const data = this._data;
|
||||
if (data?.name !== previous?.name) {
|
||||
this._suggestedName = undefined;
|
||||
}
|
||||
if (
|
||||
this._existing ||
|
||||
data?.type !== SupervisorMountType.DISK ||
|
||||
(data.device === previous?.device &&
|
||||
previous?.type === SupervisorMountType.DISK) ||
|
||||
(data.name && data.name !== this._suggestedName)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const name = mountNameFromLabel(
|
||||
this._partitions?.find((entry) => entry.partition.device === data.device)
|
||||
?.partition.label
|
||||
);
|
||||
if (name !== (data.name ?? "")) {
|
||||
data.name = name;
|
||||
}
|
||||
this._suggestedName = name;
|
||||
}
|
||||
|
||||
private async _connectMount(ev) {
|
||||
const progressButton = ev.target as HaProgressButton;
|
||||
this._error = undefined;
|
||||
|
||||
@@ -28,7 +28,7 @@ import type { ContextType } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
@@ -78,6 +78,7 @@ import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../../types";
|
||||
import { animationStyles } from "../../../resources/theme/animations.globals";
|
||||
|
||||
const RANGE_KEYS: DateRange[] = [
|
||||
"today",
|
||||
@@ -150,10 +151,6 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
|
||||
@state() private _loading = false;
|
||||
|
||||
@state() private _loadingLeaving = false;
|
||||
|
||||
@query(".loading-indicator") private _loadingIndicator?: HTMLElement;
|
||||
|
||||
private _loadingTimer?: ReturnType<typeof setTimeout>;
|
||||
|
||||
private _resizeObserver?: ResizeObserver;
|
||||
@@ -382,15 +379,10 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._loading || this._loadingLeaving
|
||||
this._loading
|
||||
? html`<ha-spinner
|
||||
class=${classMap({
|
||||
"loading-indicator": true,
|
||||
leaving: this._loadingLeaving,
|
||||
})}
|
||||
class="loading-indicator"
|
||||
size="small"
|
||||
@transitionend=${this._loadingTransitionDone}
|
||||
@transitioncancel=${this._loadingTransitionDone}
|
||||
></ha-spinner>`
|
||||
: nothing
|
||||
}
|
||||
@@ -735,10 +727,6 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
|
||||
private _updateDates(energyData: EnergyData): void {
|
||||
clearTimeout(this._loadingTimer);
|
||||
if (this._loading) {
|
||||
this._loadingLeaving = true;
|
||||
this._removeLoadingIndicatorWithoutTransition();
|
||||
}
|
||||
this._loading = false;
|
||||
this._compare = energyData.startCompare !== undefined;
|
||||
this._startDate = energyData.start;
|
||||
@@ -771,30 +759,10 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
// Basically to ensure there's no "flash loading" when data is loaded quickly
|
||||
clearTimeout(this._loadingTimer);
|
||||
this._loadingTimer = setTimeout(() => {
|
||||
this._loadingLeaving = false;
|
||||
this._loading = true;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
private async _removeLoadingIndicatorWithoutTransition() {
|
||||
await this.updateComplete;
|
||||
// No transition starts when the spinner was still at opacity 0
|
||||
this._removeIdleLoadingIndicator();
|
||||
}
|
||||
|
||||
private _loadingTransitionDone(ev: TransitionEvent) {
|
||||
if (ev.propertyName === "opacity") {
|
||||
this._removeIdleLoadingIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
private _removeIdleLoadingIndicator() {
|
||||
// Reversing the fade-in cancels a transition while the fade-out runs
|
||||
if (!this._loadingIndicator?.getAnimations().length) {
|
||||
this._loadingLeaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _getDatePickerPlacement = memoizeOne(
|
||||
(
|
||||
verticalOpeningDirection: VerticalOpeningDirection | undefined,
|
||||
@@ -810,6 +778,7 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
${animationStyles}
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -874,15 +843,7 @@ export class HuiEnergyPeriodSelector extends LitElement {
|
||||
position: absolute;
|
||||
right: var(--ha-space-2);
|
||||
align-items: center;
|
||||
transition: opacity var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
@starting-style {
|
||||
.loading-indicator {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.loading-indicator.leaving {
|
||||
opacity: 0;
|
||||
animation: fade-in var(--ha-animation-duration-normal) ease-in-out;
|
||||
}
|
||||
.date-actions .overflow {
|
||||
display: flex;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// Install the Intl.PluralRules polyfill together with its locale data.
|
||||
// FormatJS replaces Intl.PluralRules as soon as polyfill-force runs, and a
|
||||
// data-less polyfill throws from select(). Keeping both imports in one module
|
||||
// closes that gap for every caller, including the should-polyfill probes
|
||||
// that other entry bundles run.
|
||||
import "@formatjs/intl-pluralrules/polyfill-force";
|
||||
// Locale data for plural rules breaks current JSON conversions as it includes functions,
|
||||
// so only import English to avoid huge bundles
|
||||
// TODO: Setup JS imports instead of JSON fetches
|
||||
import "@formatjs/intl-pluralrules/locale-data/en";
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
polyfillTimeZoneData,
|
||||
} from "./locale-data-polyfill";
|
||||
|
||||
let polyfilled = false;
|
||||
|
||||
const _polyfillTimeZoneData = polyfillTimeZoneData;
|
||||
|
||||
// The DateTimeFormat and DurationFormat polyfills call BigInt() when formatting
|
||||
@@ -45,6 +47,10 @@ const setDefaultTimeZone = () => {
|
||||
};
|
||||
|
||||
const polyfillIntl = async () => {
|
||||
if (polyfilled) {
|
||||
return;
|
||||
}
|
||||
polyfilled = true;
|
||||
const locale = getLocalLanguage();
|
||||
const polyfills: Promise<unknown>[] = [];
|
||||
if (shouldPolyfillGetCanonicalLocales()) {
|
||||
@@ -73,10 +79,14 @@ const polyfillIntl = async () => {
|
||||
polyfills.push(import("@formatjs/intl-numberformat/polyfill-force"));
|
||||
}
|
||||
if (shouldPolyfillPluralRules(locale)) {
|
||||
// The polyfill and its English data are one chunk, so Intl.PluralRules
|
||||
// is never installed without data. Its should-polyfill probe calls
|
||||
// select(), which throws on a data-less polyfill.
|
||||
polyfills.push(import("./intl-pluralrules"));
|
||||
polyfills.push(
|
||||
import("@formatjs/intl-pluralrules/polyfill-force").then(
|
||||
// Locale data for plural rules breaks current JSON conversions as it includes functions,
|
||||
// so only import English to avoid huge bundles
|
||||
// TODO: Setup JS imports instead of JSON fetches
|
||||
() => import("@formatjs/intl-pluralrules/locale-data/en")
|
||||
)
|
||||
);
|
||||
}
|
||||
if (shouldPolyfillRelativeTimeFormat(locale)) {
|
||||
polyfills.push(import("@formatjs/intl-relativetimeformat/polyfill-force"));
|
||||
@@ -90,9 +100,4 @@ const polyfillIntl = async () => {
|
||||
);
|
||||
};
|
||||
|
||||
// Each legacy entry bundle (core, app, authorize, onboarding) carries its own
|
||||
// copy of this module, and the bundles load in parallel. A module-level flag
|
||||
// only stops re-runs within one bundle, so a second bundle could probe
|
||||
// Intl.* while the first was still loading locale data. Share one promise
|
||||
// per page instead, so every bundle waits for the same run.
|
||||
await (globalThis.__haIntlPolyfill ??= polyfillIntl());
|
||||
await polyfillIntl();
|
||||
+11
-14
@@ -17,7 +17,6 @@
|
||||
"climate": "Climate",
|
||||
"home": "Overview",
|
||||
"maintenance": "Maintenance",
|
||||
"marketplace": "Marketplace",
|
||||
"notfound": "Page not found"
|
||||
},
|
||||
"state": {
|
||||
@@ -1663,9 +1662,7 @@
|
||||
"no_items": "No items available",
|
||||
"unknown_item": "Unknown item",
|
||||
"search_or_custom": "Search | Add custom item",
|
||||
"add_custom_item": "Add custom item",
|
||||
"result_count": "{count} {count, plural,\n one {result}\n other {results}\n} for {term}",
|
||||
"no_results": "No results for {term}"
|
||||
"add_custom_item": "Add custom item"
|
||||
},
|
||||
"suggest_with_ai": {
|
||||
"label": "Suggest",
|
||||
@@ -3952,15 +3949,15 @@
|
||||
"addon_repositories": "[%key:ui::panel::config::backup::overview::progress::description::create_backup::app_repositories%]",
|
||||
"addons": "[%key:ui::panel::config::backup::overview::progress::description::create_backup::apps%]",
|
||||
"await_addon_restarts": "[%key:ui::panel::config::backup::overview::progress::description::create_backup::await_app_restarts%]",
|
||||
"app_repositories": "Backing up app repositories...",
|
||||
"apps": "Backing up apps...",
|
||||
"await_app_restarts": "Waiting for apps to restart...",
|
||||
"docker_config": "Backing up Docker configuration...",
|
||||
"finishing_file": "Finishing backup file...",
|
||||
"folders": "Backing up folders...",
|
||||
"home_assistant": "Backing up Home Assistant...",
|
||||
"upload_to_agents": "Uploading backup...",
|
||||
"cleaning_up": "Cleaning up..."
|
||||
"app_repositories": "Backing up app repositories",
|
||||
"apps": "Backing up apps",
|
||||
"await_app_restarts": "Waiting for apps to restart",
|
||||
"docker_config": "Backing up Docker configuration",
|
||||
"finishing_file": "Finishing backup file",
|
||||
"folders": "Backing up folders",
|
||||
"home_assistant": "Backing up Home Assistant",
|
||||
"upload_to_agents": "Uploading to locations",
|
||||
"cleaning_up": "Cleaning up"
|
||||
},
|
||||
"restore_backup": {
|
||||
"addon_repositories": "[%key:ui::panel::config::backup::overview::progress::description::restore_backup::app_repositories%]",
|
||||
@@ -3988,7 +3985,7 @@
|
||||
"apps": "Apps",
|
||||
"media": "Media",
|
||||
"upload": "Uploading backup",
|
||||
"cleaning_up": "Cleanup"
|
||||
"cleaning_up": "Cleaning up"
|
||||
},
|
||||
"agent_status": {
|
||||
"uploading": "Uploading...",
|
||||
|
||||
@@ -41,8 +41,6 @@ declare global {
|
||||
var __HASS_URL__: string;
|
||||
// Read by intl-polyfill before a polyfill replaces Intl.DateTimeFormat
|
||||
var __haNativeTimeZone: string | undefined;
|
||||
// Shared by every legacy entry bundle so the Intl polyfills load once
|
||||
var __haIntlPolyfill: Promise<void> | undefined;
|
||||
// Handshake between the app entry and the extra modules in index.html
|
||||
var __haScopedRegistryReady: boolean | undefined;
|
||||
var __haLoadExtraModules: (() => void) | undefined;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { searchResultsStatus } from "../../../src/components/list/search-results-status";
|
||||
import { mockLocalize } from "../../fixtures/hass";
|
||||
|
||||
interface Row {
|
||||
id: string;
|
||||
// Like "Add custom item" or "Add new area", offered for any search
|
||||
action?: boolean;
|
||||
}
|
||||
|
||||
const isResult = (row: Row) => !row.action;
|
||||
|
||||
describe("searchResultsStatus", () => {
|
||||
it("counts only the rows that are results", () => {
|
||||
const rows: (Row | string)[] = [
|
||||
"Lights",
|
||||
{ id: "kitchen_light" },
|
||||
{ id: "bedroom_light" },
|
||||
{ id: "add_new", action: true },
|
||||
];
|
||||
expect(searchResultsStatus("light", rows, isResult, mockLocalize)).toBe(
|
||||
'ui.components.combo-box.result_count: {"count":2,"term":"‘light’"}'
|
||||
);
|
||||
expect(
|
||||
searchResultsStatus(
|
||||
"garage",
|
||||
["Lights", { id: "add_new", action: true }],
|
||||
isResult,
|
||||
mockLocalize
|
||||
)
|
||||
).toBe('ui.components.combo-box.no_results: {"term":"‘garage’"}');
|
||||
});
|
||||
|
||||
it("uses a not found label that takes the search", () => {
|
||||
expect(
|
||||
searchResultsStatus(
|
||||
"garage",
|
||||
[],
|
||||
isResult,
|
||||
mockLocalize,
|
||||
(search) => `No areas found for ‘${search}’`
|
||||
)
|
||||
).toBe("No areas found for ‘garage’");
|
||||
});
|
||||
|
||||
// A live region is only read out when its text changes.
|
||||
it("names the search, so a new search with as many results is read out", () => {
|
||||
expect(
|
||||
searchResultsStatus("bedroom", [{ id: "a" }], isResult, mockLocalize)
|
||||
).not.toBe(
|
||||
searchResultsStatus("fan", [{ id: "b" }], isResult, mockLocalize)
|
||||
);
|
||||
});
|
||||
|
||||
it("is undefined without a search", () => {
|
||||
expect(
|
||||
searchResultsStatus("", [{ id: "a" }], isResult, mockLocalize)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user