1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-02-14 23:18:21 +00:00

Migrate ha-md-button-menu to ha-dropdown in 6 files (#29137)

Refactor dropdown menus to use ha-dropdown and ha-dropdown-item components

- Replaced ha-md-button-menu and related components with ha-dropdown and ha-dropdown-item in dialog-edit-sidebar, hass-tabs-subpage-data-table, ha-config-devices-dashboard, ha-config-entities.
- Updated event handling to accommodate new dropdown structure.
- Added wa-divider for better visual separation in dropdowns.
- Improved accessibility and usability of dropdown menus across various components.
This commit is contained in:
Wendelin
2026-01-22 20:00:51 +01:00
committed by GitHub
parent 2599804d22
commit de85b08de4
6 changed files with 529 additions and 484 deletions

View File

@@ -1,16 +1,16 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { mdiDotsVertical } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-md-button-menu";
import "./ha-dropdown";
import "./ha-dropdown-item";
import "./ha-icon-button";
import "./ha-md-divider";
import "./ha-svg-icon";
import "./ha-tooltip";
import "./ha-md-menu-item";
import "./ha-md-divider";
export interface IconOverflowMenuItem {
[key: string]: any;
@@ -39,10 +39,7 @@ export class HaIconOverflowMenu extends LitElement {
return html`
${this.narrow
? html` <!-- Collapsed representation for small screens -->
<ha-md-button-menu
@click=${this._handleIconOverflowMenuOpened}
positioning="popover"
>
<ha-dropdown @wa-show=${this._handleIconOverflowMenuOpened}>
<ha-icon-button
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
@@ -51,24 +48,17 @@ export class HaIconOverflowMenu extends LitElement {
${this.items.map((item) =>
item.divider
? html`<ha-md-divider
role="separator"
tabindex="-1"
></ha-md-divider>`
: html`<ha-md-menu-item
? html`<wa-divider></wa-divider>`
: html`<ha-dropdown-item
?disabled=${item.disabled}
.clickAction=${item.action}
class=${classMap({ warning: Boolean(item.warning) })}
@click=${item.action}
variant=${item.warning ? "danger" : "default"}
>
<ha-svg-icon
slot="start"
class=${classMap({ warning: Boolean(item.warning) })}
.path=${item.path}
></ha-svg-icon>
<ha-svg-icon slot="icon" .path=${item.path}></ha-svg-icon>
${item.label}
</ha-md-menu-item>`
</ha-dropdown-item>`
)}
</ha-md-button-menu>`
</ha-dropdown>`
: html`
<!-- Icon representation for big screens -->
${this.items.map((item) =>

View File

@@ -24,11 +24,12 @@ import {
import { VolumeSliderController } from "../../../common/util/volume-slider";
import "../../../components/chips/ha-assist-chip";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-marquee-text";
import "../../../components/ha-md-button-menu";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-select";
import type { HaSlider } from "../../../components/ha-slider";
import "../../../components/ha-svg-icon";
@@ -201,29 +202,30 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
return html`<ha-md-button-menu positioning="popover">
return html`<ha-dropdown>
<ha-icon-button
slot="trigger"
.title=${this.hass.localize(`ui.card.media_player.source`)}
.label=${this.hass.localize(`ui.card.media_player.source`)}
.path=${mdiLoginVariant}
@wa-select=${this._handleSourceChange}
>
</ha-icon-button>
${this.stateObj.attributes.source_list!.map(
(source) =>
html`<ha-md-menu-item
data-source=${source}
@click=${this._handleSourceClick}
@keydown=${this._handleSourceClick}
.selected=${source === this.stateObj?.attributes.source}
html`<ha-dropdown-item
.value=${source}
class=${source === this.stateObj?.attributes.source
? "selected"
: ""}
>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"source",
source
)}
</ha-md-menu-item>`
</ha-dropdown-item>`
)}
</ha-md-button-menu>`;
</ha-dropdown>`;
}
protected _renderSoundMode() {
@@ -238,29 +240,29 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
return html`<ha-md-button-menu positioning="popover">
return html`<ha-dropdown @wa-select=${this._handleSoundModeChange}>
<ha-icon-button
slot="trigger"
.title=${this.hass.localize(`ui.card.media_player.sound_mode`)}
.label=${this.hass.localize(`ui.card.media_player.sound_mode`)}
.path=${mdiMusicNoteEighth}
>
</ha-icon-button>
${this.stateObj.attributes.sound_mode_list!.map(
(soundMode) =>
html`<ha-md-menu-item
data-sound-mode=${soundMode}
@click=${this._handleSoundModeClick}
@keydown=${this._handleSoundModeClick}
.selected=${soundMode === this.stateObj?.attributes.sound_mode}
html`<ha-dropdown-item
.value=${soundMode}
class=${soundMode === this.stateObj?.attributes.sound_mode
? "selected"
: ""}
>
${this.hass.formatEntityAttributeValue(
this.stateObj!,
"sound_mode",
soundMode
)}
</ha-md-menu-item>`
</ha-dropdown-item>`
)}
</ha-md-button-menu>`;
</ha-dropdown>`;
}
protected _renderGrouping() {
@@ -677,6 +679,13 @@ class MoreInfoMediaPlayer extends LitElement {
align-self: center;
width: 320px;
}
ha-dropdown-item.selected {
font-weight: var(--ha-font-weight-medium);
color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
--icon-primary-color: var(--primary-color);
}
`;
private _handleClick(e: MouseEvent): void {
@@ -734,8 +743,8 @@ class MoreInfoMediaPlayer extends LitElement {
});
}
private _handleSourceClick(e: Event) {
const source = (e.currentTarget as HTMLElement).getAttribute("data-source");
private _handleSourceChange(e: CustomEvent<{ item: HaDropdownItem }>) {
const source = e.detail.item.value;
if (!source || this.stateObj!.attributes.source === source) {
return;
}
@@ -746,10 +755,8 @@ class MoreInfoMediaPlayer extends LitElement {
});
}
private _handleSoundModeClick(e: Event) {
const soundMode = (e.currentTarget as HTMLElement).getAttribute(
"data-sound-mode"
);
private _handleSoundModeChange(ev: CustomEvent<{ item: HaDropdownItem }>) {
const soundMode = ev.detail.item.value;
if (!soundMode || this.stateObj!.attributes.sound_mode === soundMode) {
return;
}

View File

@@ -7,6 +7,8 @@ import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert";
import "../../components/ha-button";
import "../../components/ha-dialog-footer";
import "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import "../../components/ha-fade-in";
import "../../components/ha-icon-button";
import "../../components/ha-items-display-editor";
@@ -14,12 +16,10 @@ import type {
DisplayItem,
DisplayValue,
} from "../../components/ha-items-display-editor";
import "../../components/ha-md-button-menu";
import "../../components/ha-md-menu-item";
import "../../components/ha-wa-dialog";
import { computePanels } from "../../components/ha-sidebar";
import "../../components/ha-spinner";
import "../../components/ha-svg-icon";
import "../../components/ha-wa-dialog";
import {
fetchFrontendUserData,
saveFrontendUserData,
@@ -176,22 +176,17 @@ class DialogEditSidebar extends LitElement {
: ""}
@closed=${this._dialogClosed}
>
<ha-md-button-menu
slot="headerActionItems"
positioning="popover"
anchor-corner="end-end"
menu-corner="start-end"
>
<ha-dropdown slot="headerActionItems" placement="bottom-end">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item .clickAction=${this._resetToDefaults}>
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
<ha-dropdown-item @click=${this._resetToDefaults}>
<ha-svg-icon slot="icon" .path=${mdiRestart}></ha-svg-icon>
${this.hass.localize("ui.sidebar.reset_to_defaults")}
</ha-md-menu-item>
</ha-md-button-menu>
</ha-dropdown-item>
</ha-dropdown>
<div class="content">${this._renderContent()}</div>
<ha-dialog-footer slot="footer">
<ha-button

View File

@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import {
mdiArrowDown,
@@ -28,9 +29,9 @@ import type {
import { showDataTableSettingsDialog } from "../components/data-table/show-dialog-data-table-settings";
import "../components/ha-dialog";
import "../components/ha-dialog-header";
import "../components/ha-md-button-menu";
import "../components/ha-md-divider";
import "../components/ha-md-menu-item";
import "../components/ha-dropdown";
import "../components/ha-dropdown-item";
import type { HaDropdownItem } from "../components/ha-dropdown-item";
import "../components/search-input-outlined";
import { KeyboardShortcutMixin } from "../mixins/keyboard-shortcut-mixin";
import type { HomeAssistant, Route } from "../types";
@@ -254,7 +255,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
const sortByMenu = Object.values(this.columns).find((col) => col.sortable)
? html`
<ha-md-button-menu positioning="popover">
<ha-dropdown @wa-select=${this._handleSortBy}>
<ha-assist-chip
slot="trigger"
.label=${localize("ui.components.subpage-data-table.sort_by", {
@@ -273,12 +274,8 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
${Object.entries(this.columns).map(([id, column]) =>
column.sortable
? html`
<ha-md-menu-item
<ha-dropdown-item
.value=${id}
@click=${this._handleSortBy}
@keydown=${this._handleSortBy}
keep-open
.selected=${id === this._sortColumn}
class=${classMap({ selected: id === this._sortColumn })}
>
${this._sortColumn === id
@@ -292,17 +289,17 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
`
: nothing}
${column.title || column.label}
</ha-md-menu-item>
</ha-dropdown-item>
`
: nothing
)}
</ha-md-button-menu>
</ha-dropdown>
`
: nothing;
const groupByMenu = Object.values(this.columns).find((col) => col.groupable)
? html`
<ha-md-button-menu positioning="popover">
<ha-dropdown @wa-select=${this._handleGroupBy}>
<ha-assist-chip
.label=${localize("ui.components.subpage-data-table.group_by", {
groupColumn:
@@ -320,49 +317,47 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
${Object.entries(this.columns).map(([id, column]) =>
column.groupable
? html`
<ha-md-menu-item
<ha-dropdown-item
.value=${id}
.clickAction=${this._handleGroupBy}
.selected=${id === this._groupColumn}
class=${classMap({ selected: id === this._groupColumn })}
>
${column.title || column.label}
</ha-md-menu-item>
</ha-dropdown-item>
`
: nothing
)}
<ha-md-menu-item
.value=${""}
.clickAction=${this._handleGroupBy}
.selected=${!this._groupColumn}
<ha-dropdown-item
value="reset"
class=${classMap({ selected: !this._groupColumn })}
>
${localize("ui.components.subpage-data-table.dont_group_by")}
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.clickAction=${this._collapseAllGroups}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item
value="collapse_all"
.disabled=${!this._groupColumn}
>
<ha-svg-icon
slot="start"
slot="icon"
.path=${mdiUnfoldLessHorizontal}
></ha-svg-icon>
${localize(
"ui.components.subpage-data-table.collapse_all_groups"
)}
</ha-md-menu-item>
<ha-md-menu-item
.clickAction=${this._expandAllGroups}
</ha-dropdown-item>
<ha-dropdown-item
value="expand_all"
.disabled=${!this._groupColumn}
>
<ha-svg-icon
slot="start"
slot="icon"
.path=${mdiUnfoldMoreHorizontal}
></ha-svg-icon>
${localize("ui.components.subpage-data-table.expand_all_groups")}
</ha-md-menu-item>
</ha-md-button-menu>
</ha-dropdown-item>
</ha-dropdown>
`
: nothing;
@@ -399,7 +394,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
"ui.components.subpage-data-table.exit_selection_mode"
)}
></ha-icon-button>
<ha-md-button-menu>
<ha-dropdown @wa-select=${this._handleSelect}>
<ha-assist-chip
.label=${localize(
"ui.components.subpage-data-table.select"
@@ -415,36 +410,19 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
<ha-md-menu-item
.value=${undefined}
.clickAction=${this._selectAll}
>
<div slot="headline">
${localize("ui.components.subpage-data-table.select_all")}
</div>
</ha-md-menu-item>
<ha-md-menu-item
.value=${undefined}
.clickAction=${this._selectNone}
>
<div slot="headline">
${localize(
"ui.components.subpage-data-table.select_none"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item
.value=${undefined}
.clickAction=${this._disableSelectMode}
>
<div slot="headline">
${localize(
"ui.components.subpage-data-table.exit_selection_mode"
)}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
<ha-dropdown-item value="all">
${localize("ui.components.subpage-data-table.select_all")}
</ha-dropdown-item>
<ha-dropdown-item value="none">
${localize("ui.components.subpage-data-table.select_none")}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="disable_select_mode">
${localize(
"ui.components.subpage-data-table.exit_selection_mode"
)}
</ha-dropdown-item>
</ha-dropdown>
${this.selected !== undefined
? html`<p>
${localize("ui.components.subpage-data-table.selected", {
@@ -612,10 +590,10 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
this._sortColumn = this._sortDirection ? ev.detail.column : undefined;
}
private _handleSortBy(ev) {
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") return;
private _handleSortBy(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.preventDefault(); // keep the dropdown open
const columnId = ev.currentTarget.value;
const columnId = ev.detail.item.value;
if (!this._sortDirection || this._sortColumn !== columnId) {
this._sortDirection = "asc";
} else if (this._sortDirection === "asc") {
@@ -631,9 +609,24 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
});
}
private _handleGroupBy = (item) => {
this._setGroupColumn(item.value);
};
private _handleGroupBy(ev: CustomEvent<{ item: HaDropdownItem }>) {
const group = ev.detail.item.value;
if (group === "reset") {
this._setGroupColumn("");
return;
}
if (group === "collapse_all") {
this._collapseAllGroups();
return;
}
if (group === "expand_all") {
this._expandAllGroups();
return;
}
this._setGroupColumn(group);
}
private _setGroupColumn(columnId: string) {
this._groupColumn = columnId;
@@ -669,6 +662,26 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
this._selectMode = true;
}
private _handleSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail.item.value;
if (!action) {
return;
}
switch (action) {
case "all":
this._selectAll();
break;
case "none":
this._selectNone();
break;
case "disable_select_mode":
this._disableSelectMode();
break;
}
}
private _disableSelectMode = () => {
this._selectMode = false;
this._dataTable.clearSelection();
@@ -902,9 +915,17 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
flex-direction: column;
}
ha-md-button-menu ha-assist-chip {
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-dropdown-item.selected {
border: 1px solid var(--primary-color);
font-weight: var(--ha-font-weight-medium);
color: var(--primary-color);
background-color: var(--ha-color-fill-primary-quiet-resting);
--icon-primary-color: var(--primary-color);
}
`;
}

View File

@@ -1,7 +1,7 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiCancel,
mdiChevronRight,
mdiDelete,
mdiDotsVertical,
mdiMenuDown,
@@ -43,6 +43,9 @@ import "../../../components/data-table/ha-data-table-labels";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-alert";
import "../../../components/ha-check-list-item";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-fab";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-floor-areas";
@@ -50,8 +53,6 @@ import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
@@ -702,6 +703,70 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
];
}
private _renderAreaItems = (slot = "") =>
html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-dropdown-item .value=${`area_${area.area_id}`} .slot=${slot}>
${area.icon
? html`<ha-icon slot="icon" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiTextureBox}
></ha-svg-icon>`}
${area.name}
</ha-dropdown-item>`
)}
<ha-dropdown-item value="area_no" .slot=${slot}>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider .slot=${slot}></wa-divider>
<ha-dropdown-item value="area_create" .slot=${slot}>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</ha-dropdown-item>`;
private _renderLabelItems = (slot = "") =>
html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
return html`<ha-dropdown-item
.slot=${slot}
.value=${`label_${label.label_id}`}
.action=${selected ? "remove" : "add"}
keep-open
>
<ha-checkbox
slot="icon"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description || undefined}
>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-dropdown-item>`;
})}
<wa-divider .slot=${slot}></wa-divider>
<ha-dropdown-item value="label_create" .slot=${slot}>
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
protected render(): TemplateResult {
const { devicesOutput } = this._devicesAndFilterDomains(
this.hass.devices,
@@ -718,77 +783,6 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
(this._sizeController.value && this._sizeController.value < 700) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const areaItems = html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-md-menu-item
.value=${area.area_id}
.clickAction=${this._handleBulkArea}
>
${area.icon
? html`<ha-icon slot="start" .icon=${area.icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${area.name}</div>
</ha-md-menu-item>`
)}
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateArea}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</div>
</ha-md-menu-item>`;
const labelItems = html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((deviceId) =>
this.hass.devices[deviceId]?.labels.includes(label.label_id)
);
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-md-menu-item>`;
})}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-md-menu-item
>`;
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
@@ -906,7 +900,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
></ha-filter-labels>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
? html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleBulkLabel}
>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -918,12 +915,15 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-md-button-menu>
${this._renderLabelItems()}
</ha-dropdown>
${areasInOverflow
? nothing
: html`<ha-md-button-menu slot="selection-bar">
: html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleBulkArea}
>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -935,10 +935,10 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-md-button-menu>`}`
${this._renderAreaItems()}
</ha-dropdown>`}`
: nothing}
<ha-md-button-menu has-overflow slot="selection-bar">
<ha-dropdown slot="selection-bar" @wa-select=${this._handleBulkAction}>
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
@@ -959,51 +959,33 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html` <ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
</ha-sub-menu>`
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
${this._renderLabelItems("submenu")}
</ha-dropdown-item>`
: nothing}
${areasInOverflow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${areaItems}</ha-md-menu>
</ha-sub-menu>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
${this._renderAreaItems("submenu")}
</ha-dropdown-item>
<wa-divider></wa-divider>`
: nothing}
<ha-md-menu-item
.clickAction=${this._deleteSelected}
<ha-dropdown-item
value="delete_selected"
.disabled=${!this._selectedCanDelete.length}
class="warning"
variant="danger"
>
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.delete_selected.button"
)}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.delete_selected.button"
)}
</ha-dropdown-item>
</ha-dropdown>
</hass-tabs-subpage-data-table>
`;
}
@@ -1094,12 +1076,22 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value;
}
private _handleBulkArea = (item) => {
const area = item.value;
this._bulkAddArea(area);
};
private _handleBulkArea(ev: CustomEvent<{ item: HaDropdownItem }>) {
const area = ev.detail.item.value;
private async _bulkAddArea(area: string) {
if (area === "area_create") {
this._bulkCreateArea();
return;
}
if (area === "area_no") {
this._bulkAddArea(null);
return;
}
this._bulkAddArea(area.substring(5));
}
private async _bulkAddArea(area: string | null) {
const promises: Promise<DeviceRegistryEntry>[] = [];
this._selected.forEach((deviceId) => {
promises.push(
@@ -1134,10 +1126,20 @@ ${rejected
});
};
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
private async _handleBulkLabel(ev: CustomEvent<{ item: HaDropdownItem }>) {
const label = ev.detail.item.value;
if (label === "label_create") {
this._bulkCreateLabel();
return;
}
if (!label) {
return;
}
const action = (ev.detail.item as any).action;
this._bulkLabel(label.substring(6), action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
@@ -1251,6 +1253,27 @@ ${rejected
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
private _handleBulkAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail.item.value;
if (!action) {
return;
}
if (action === "delete_selected") {
this._deleteSelected();
}
if (action.startsWith("label_")) {
this._handleBulkLabel(ev);
return;
}
if (action.startsWith("area_")) {
this._handleBulkArea(ev);
}
}
static get styles(): CSSResultGroup {
return [
css`
@@ -1273,7 +1296,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-md-button-menu ha-assist-chip {
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {

View File

@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAlertCircle,
@@ -57,6 +58,9 @@ import type {
import "../../../components/data-table/ha-data-table-labels";
import "../../../components/ha-alert";
import "../../../components/ha-check-list-item";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../../components/ha-dropdown-item";
import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-domains";
import "../../../components/ha-filter-floor-areas";
@@ -66,8 +70,6 @@ import "../../../components/ha-filter-states";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
@@ -87,6 +89,7 @@ import type {
import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry";
import type { EntitySources } from "../../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import { HELPERS_CRUD } from "../../../data/helpers_crud";
import type { IntegrationManifest } from "../../../data/integration";
import {
@@ -116,12 +119,11 @@ import { isHelperDomain } from "../helpers/const";
import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants";
import {
getAssistantsTableColumn,
getAssistantsSortableKey,
getAssistantsTableColumn,
} from "../voice-assistants/expose/assistants-table-column";
import { getAvailableAssistants } from "../voice-assistants/expose/available-assistants";
export interface StateEntity extends Omit<
EntityRegistryEntry,
@@ -788,6 +790,44 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
];
}
private _renderLabelItems = (slot = "") =>
html`${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-dropdown-item
.slot=${slot}
.value=${`label_${label.label_id}`}
.action=${selected ? "remove" : "add"}
>
<ha-checkbox
slot="icon"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-dropdown-item>`;
})}
<wa-divider .slot=${slot}></wa-divider>
<ha-dropdown-item .slot=${slot} value="label_create">
${this.hass.localize("ui.panel.config.labels.add_label")}
</ha-dropdown-item>`;
protected render() {
if (!this.hass || this._entities === undefined) {
return html` <hass-loading-screen></hass-loading-screen> `;
@@ -812,53 +852,13 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
[...filteredDomains][0]
);
const labelItems = html` ${this._labels?.map((label) => {
const color = label.color ? computeCssColor(label.color) : undefined;
const selected = this._selected.every((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
const partial =
!selected &&
this._selected.some((entityId) =>
this.hass.entities[entityId]?.labels.includes(label.label_id)
);
return html`<ha-md-menu-item
.value=${label.label_id}
.action=${selected ? "remove" : "add"}
@click=${this._handleBulkLabel}
keep-open
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
reducedTouchTarget
></ha-checkbox>
<ha-label
style=${color ? `--color: ${color}` : ""}
.description=${label.description}
>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
${label.name}
</ha-label>
</ha-md-menu-item>`;
})}
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateLabel}>
<div slot="headline">
${this.hass.localize("ui.panel.config.labels.add_label")}
</div></ha-md-menu-item
>`;
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${
this._searchParms.has("historyBack") ? undefined : "/config"
}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
.route=${this.route}
.tabs=${configSections.devices}
.columns=${this._columns(this.hass.localize, filteredEntities)}
@@ -868,16 +868,14 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
{ number: filteredEntities.length }
)}
has-filters
.filters=${
Object.values(this._filters).filter((filter) =>
Array.isArray(filter)
? filter.length
: filter &&
Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val
)
).length
}
.filters=${Object.values(this._filters).filter((filter) =>
Array.isArray(filter)
? filter.length
: filter &&
Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
selectable
.selected=${this._selected.length}
.initialGroupColumn=${this._activeGrouping ?? "device_full"}
@@ -904,157 +902,125 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
slot="toolbar-icon"
></ha-integration-overflow-menu>
${
!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
</ha-assist-chip>
${labelItems}
</ha-md-button-menu>`
: nothing
}
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
>
<ha-svg-icon slot="trailing-icon" .path=${mdiMenuDown}></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
></ha-icon-button>`
}
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon
></ha-assist-chip>
${
this.narrow
? html`<ha-sub-menu>
<ha-md-menu-item slot="item">
<div slot="headline">
${this.hass.localize(
${!this.narrow
? html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleBulkLabel}
>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
</div>
<ha-svg-icon slot="end" .path=${mdiChevronRight}></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${labelItems}</ha-md-menu>
</ha-sub-menu>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing
}
<ha-md-menu-item .clickAction=${this._enableSelected}>
<ha-svg-icon slot="start" .path=${mdiToggleSwitch}></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._disableSelected}>
<ha-svg-icon
slot="start"
.path=${mdiToggleSwitchOffOutline}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._unhideSelected}>
<ha-svg-icon
slot="start"
.path=${mdiEye}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.unhide_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-menu-item .clickAction=${this._hideSelected}>
<ha-svg-icon
slot="start"
.path=${mdiEyeOff}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._restoreEntityIdSelected}>
<ha-svg-icon
slot="start"
.path=${mdiRestore}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.restore_entity_id_selected.button"
)}
</div>
</ha-md-menu-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._removeSelected} class="warning">
<ha-svg-icon
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.entities.picker.delete_selected.button"
)}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
${
Array.isArray(this._filters.config_entry) &&
this._filters.config_entry.length
? html`<ha-alert slot="filter-pane">
${this.hass.localize(
"ui.panel.config.entities.picker.filtering_by_config_entry"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${this._renderLabelItems()}
</ha-dropdown>`
: nothing}
<ha-dropdown slot="selection-bar" @wa-select=${this._handleBulkAction}>
${this.narrow
? html`<ha-assist-chip
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
${this._entries?.find(
(entry) => entry.entry_id === this._filters.config_entry![0]
)?.title || this._filters.config_entry[0]}${this._filters
.config_entry.length === 1 &&
Array.isArray(this._filters.sub_entry) &&
this._filters.sub_entry.length
? html` (${this._subEntries?.find(
(entry) =>
entry.subentry_id === this._filters.sub_entry![0]
)?.title || this._filters.sub_entry[0]})`
: nothing}
</ha-alert>`
: nothing
}
slot="trigger"
>
<ha-svg-icon
slot="trailing-icon"
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>`
: html`<ha-icon-button
.path=${mdiDotsVertical}
.label=${this.hass.localize(
"ui.panel.config.automation.picker.bulk_action"
)}
slot="trigger"
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
${this._renderLabelItems("submenu")}
</ha-dropdown-item>
<wa-divider></wa-divider>`
: nothing}
<ha-dropdown-item value="enable_selected">
<ha-svg-icon slot="icon" .path=${mdiToggleSwitch}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.enable_selected.button"
)}
</ha-dropdown-item>
<ha-dropdown-item value="disable_selected">
<ha-svg-icon
slot="icon"
.path=${mdiToggleSwitchOffOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.disable_selected.button"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="unhide_selected">
<ha-svg-icon slot="icon" .path=${mdiEye}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.unhide_selected.button"
)}
</ha-dropdown-item>
<ha-dropdown-item value="hide_selected">
<ha-svg-icon slot="icon" .path=${mdiEyeOff}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.hide_selected.button"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="restore_entity_id_selected">
<ha-svg-icon slot="icon" .path=${mdiRestore}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.restore_entity_id_selected.button"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="delete_selected" variant="danger">
<ha-svg-icon slot="icon" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.entities.picker.delete_selected.button"
)}
</ha-dropdown-item>
</ha-dropdown>
${Array.isArray(this._filters.config_entry) &&
this._filters.config_entry.length
? html`<ha-alert slot="filter-pane">
${this.hass.localize(
"ui.panel.config.entities.picker.filtering_by_config_entry"
)}
${this._entries?.find(
(entry) => entry.entry_id === this._filters.config_entry![0]
)?.title || this._filters.config_entry[0]}${this._filters
.config_entry.length === 1 &&
Array.isArray(this._filters.sub_entry) &&
this._filters.sub_entry.length
? html` (${this._subEntries?.find(
(entry) => entry.subentry_id === this._filters.sub_entry![0]
)?.title || this._filters.sub_entry[0]})`
: nothing}
</ha-alert>`
: nothing}
<ha-filter-floor-areas
.hass=${this.hass}
type="entity"
@@ -1124,20 +1090,16 @@ ${
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
${
includeAddDeviceFab
? html`<ha-fab
.label=${this.hass.localize(
"ui.panel.config.devices.add_device"
)}
extended
@click=${this._addDevice}
slot="fab"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>`
: nothing
}
${includeAddDeviceFab
? html`<ha-fab
.label=${this.hass.localize("ui.panel.config.devices.add_device")}
extended
@click=${this._addDevice}
slot="fab"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>`
: nothing}
</hass-tabs-subpage-data-table>
`;
}
@@ -1396,10 +1358,24 @@ ${
this._clearSelection();
};
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
await this._bulkLabel(label, action);
private async _handleBulkLabel(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.preventDefault(); // Prevent the dropdown from closing
const label = ev.detail.item.value;
if (!label) {
return;
}
if (label === "label_create") {
this._bulkCreateLabel();
return;
}
const labelId = label.substring(6);
const action = (ev.detail.item as any).action;
await this._bulkLabel(labelId, action);
}
private async _bulkLabel(label: string, action: "add" | "remove") {
@@ -1590,6 +1566,39 @@ ${rejected
this._activeHiddenColumns = ev.detail.hiddenColumns;
}
private _handleBulkAction(ev: CustomEvent<{ item: HaDropdownItem }>) {
const action = ev.detail.item.value;
if (!action) {
return;
}
switch (action) {
case "enable_selected":
this._enableSelected();
return;
case "disable_selected":
this._disableSelected();
return;
case "unhide_selected":
this._unhideSelected();
return;
case "hide_selected":
this._hideSelected();
return;
case "restore_entity_id_selected":
this._restoreEntityIdSelected();
return;
case "delete_selected":
this._removeSelected();
return;
}
if (action.startsWith("label_")) {
this._handleBulkLabel(ev);
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -1659,7 +1668,7 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-md-button-menu ha-assist-chip {
ha-dropdown ha-assist-chip {
--md-assist-chip-trailing-space: 8px;
}
ha-label {