1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-24 10:51:01 +01:00

Refactor dropdown menus to use ha-dropdown and ha-dropdown-item components (#29204)

This commit is contained in:
Wendelin
2026-01-27 13:36:53 +01:00
committed by GitHub
parent 4a2b7324f7
commit 6d1d7690ef
11 changed files with 1287 additions and 1090 deletions

View File

@@ -1,7 +1,7 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { consume } from "@lit/context";
import {
mdiChevronRight,
mdiCog,
mdiContentDuplicate,
mdiDelete,
@@ -46,6 +46,9 @@ import type {
SortingChangedEvent,
} from "../../../components/data-table/ha-data-table";
import "../../../components/data-table/ha-data-table-labels";
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-blueprints";
import "../../../components/ha-filter-categories";
@@ -81,6 +84,7 @@ import type {
UpdateEntityRegistryEntryResult,
} from "../../../data/entity/entity_registry";
import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import type { LabelRegistryEntry } from "../../../data/label/label_registry";
import {
createLabelRegistryEntry,
@@ -113,12 +117,11 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
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";
type ScriptItem = ScriptEntity & {
name: string;
@@ -451,102 +454,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
}
protected render(): TemplateResult {
const categoryItems = html`${this._categories?.map(
(category) =>
html`<ha-md-menu-item
.value=${category.category_id}
.clickAction=${this._handleBulkCategory}
>
${category.icon
? html`<ha-icon slot="start" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiTag}></ha-svg-icon>`}
<div slot="headline">${category.name}</div>
</ha-md-menu-item>`
)}
<ha-md-menu-item .value=${null} .clickAction=${this._handleBulkCategory}>
<div slot="headline">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</div> </ha-md-menu-item
><ha-md-divider role="separator" tabindex="-1"></ha-md-divider>
<ha-md-menu-item .clickAction=${this._bulkCreateCategory}>
<div slot="headline">
${this.hass.localize("ui.panel.config.category.editor.add")}
</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((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
reducedTouchTarget
>
<ha-checkbox
slot="start"
.checked=${selected}
.indeterminate=${partial}
></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
>`;
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 areasInOverflow =
(this._sizeController.value && this._sizeController.value < 900) ||
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
@@ -687,7 +594,10 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
></ha-filter-blueprints>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
? html`<ha-dropdown
slot="selection-bar"
@wa-select=${this._handleBulkCategory}
>
<ha-assist-chip
slot="trigger"
.label=${this.hass.localize(
@@ -699,11 +609,14 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${categoryItems}
</ha-md-button-menu>
${this._renderCategoryItems()}
</ha-dropdown>
${labelsInOverflow
? nothing
: 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(
@@ -715,11 +628,14 @@ class HaScriptPicker 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(
@@ -731,14 +647,15 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
.path=${mdiMenuDown}
></ha-svg-icon>
</ha-assist-chip>
${areaItems}
</ha-md-button-menu>`}`
${this._renderAreaItems()}
</ha-dropdown>`}`
: nothing}
${this.narrow || areasInOverflow
? html`
<ha-md-button-menu has-overflow slot="selection-bar">
${
this.narrow
? html` <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"
@@ -756,68 +673,32 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
"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(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
</div>
<ha-svg-icon
slot="end"
.path=${mdiChevronRight}
></ha-svg-icon>
</ha-md-menu-item>
<ha-md-menu slot="menu">${categoryItems}</ha-md-menu>
</ha-sub-menu>`
: nothing
}
${
this.narrow || labelsInOverflow
? 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>`
: nothing
}
${
this.narrow || 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>`
: nothing
}
</ha-md-button-menu>`
></ha-icon-button>`}
${this.narrow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.move_category"
)}
${this._renderCategoryItems("submenu")}
</ha-dropdown-item>`
: nothing}
${this.narrow || labelsInOverflow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.add_label"
)}
${this._renderLabelItems("submenu")}
</ha-dropdown-item>`
: nothing}
${this.narrow || areasInOverflow
? html`<ha-dropdown-item>
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.move_area"
)}
${this._renderAreaItems("submenu")}
</ha-dropdown-item>`
: nothing}
</ha-dropdown>`
: nothing}
${!this.scripts.length
? html` <div class="empty" slot="empty">
@@ -1056,12 +937,22 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
this._selected = ev.detail.value;
}
private _handleBulkCategory = (item) => {
const category = item.value;
this._bulkAddCategory(category);
private _handleBulkCategory = (ev: CustomEvent<{ item: HaDropdownItem }>) => {
const value = ev.detail.item.value;
if (value === "category_create") {
this._bulkCreateCategory();
return;
}
if (value === "category_none") {
this._bulkAddCategory(null);
return;
}
if (value?.startsWith("category_")) {
this._bulkAddCategory(value.substring(9));
}
};
private async _bulkAddCategory(category: string) {
private async _bulkAddCategory(category: string | null) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
@@ -1086,11 +977,18 @@ ${rejected
}
}
private async _handleBulkLabel(ev) {
const label = ev.currentTarget.value;
const action = ev.currentTarget.action;
this._bulkLabel(label, action);
}
private _handleBulkLabel = (ev: CustomEvent<{ item: HaDropdownItem }>) => {
ev.preventDefault();
const value = ev.detail.item.value;
if (value === "label_create") {
this._bulkCreateLabel();
return;
}
if (value?.startsWith("label_")) {
const action = (ev.detail.item as any).action;
this._bulkLabel(value.substring(6), action);
}
};
private async _bulkLabel(label: string, action: "add" | "remove") {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
@@ -1296,12 +1194,22 @@ ${rejected
});
};
private _handleBulkArea = (item) => {
const area = item.value;
this._bulkAddArea(area);
private _handleBulkArea = (ev: CustomEvent<{ item: HaDropdownItem }>) => {
const value = ev.detail.item.value;
if (value === "area_create") {
this._bulkCreateArea();
return;
}
if (value === "area_none") {
this._bulkAddArea(null);
return;
}
if (value?.startsWith("area_")) {
this._bulkAddArea(value.substring(5));
}
};
private async _bulkAddArea(area: string) {
private async _bulkAddArea(area: string | null) {
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
this._selected.forEach((entityId) => {
promises.push(
@@ -1336,6 +1244,132 @@ ${rejected
});
};
private _renderCategoryItems = (slot = "") =>
html`${this._categories?.map(
(category) =>
html`<ha-dropdown-item
.slot=${slot}
.value=${`category_${category.category_id}`}
>
${category.icon
? html`<ha-icon slot="icon" .icon=${category.icon}></ha-icon>`
: html`<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>`}
${category.name}
</ha-dropdown-item>`
)}
<ha-dropdown-item .slot=${slot} value="category_none">
${this.hass.localize(
"ui.panel.config.automation.picker.bulk_actions.no_category"
)}
</ha-dropdown-item>
<wa-divider .slot=${slot}></wa-divider>
<ha-dropdown-item .slot=${slot} value="category_create">
${this.hass.localize("ui.panel.config.category.editor.add")}
</ha-dropdown-item>`;
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>`;
private _renderAreaItems = (slot = "") =>
html`${Object.values(this.hass.areas).map(
(area) =>
html`<ha-dropdown-item .slot=${slot} .value=${`area_${area.area_id}`}>
${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 .slot=${slot} value="area_none">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.no_area"
)}
</ha-dropdown-item>
<wa-divider .slot=${slot}></wa-divider>
<ha-dropdown-item .slot=${slot} value="area_create">
${this.hass.localize(
"ui.panel.config.devices.picker.bulk_actions.add_area"
)}
</ha-dropdown-item>`;
private _handleBulkAction = (ev) => {
const item = ev.detail.item;
const value = item.value;
if (!value) {
return;
}
if (value.startsWith("category_")) {
if (value === "category_create") {
this._bulkCreateCategory();
} else if (value === "category_none") {
this._bulkAddCategory(null);
} else {
this._bulkAddCategory(value.substring(9));
}
return;
}
if (value.startsWith("label_")) {
if (value === "label_create") {
this._bulkCreateLabel();
} else {
const action = item.action;
this._bulkLabel(value.substring(6), action);
}
return;
}
if (value.startsWith("area_")) {
if (value === "area_create") {
this._bulkCreateArea();
} else if (value === "area_none") {
this._bulkAddArea(null);
} else {
this._bulkAddArea(value.substring(5));
}
}
};
private _handleSortingChanged(ev: CustomEvent) {
this._activeSorting = ev.detail;
}
@@ -1383,7 +1417,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 {