Replace ha-md-button-menu with ha-dropdown (#29210)

This commit is contained in:
Wendelin 2026-01-27 16:10:32 +01:00 committed by GitHub
parent 6e1999ceb7
commit cddf91cfd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 73 additions and 214 deletions

View File

@ -1,17 +1,18 @@
import { mdiMenuDown, mdiMenuUp } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-check-list-item";
import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-dropdown";
import "../ha-dropdown-item";
import "../ha-formfield";
import "../ha-icon-button";
import "../ha-md-button-menu";
import "../ha-md-menu-item";
import "../ha-textfield";
import "../ha-picker-field";
import type { HaDropdown } from "../ha-dropdown";
import type { HaDropdownItem } from "../ha-dropdown-item";
import type {
HaFormElement,
HaFormMultiSelectData,
@ -36,16 +37,12 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
@property() public label!: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, reflect: true }) public disabled = false;
@state() private _opened = false;
@query("ha-md-button-menu") private _input?: HTMLElement;
@query("ha-dropdown") private _dropdown!: HaDropdown;
public focus(): void {
if (this._input) {
this._input.focus();
}
this._dropdown?.focus();
}
protected render(): TemplateResult {
@ -74,13 +71,14 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
}
return html`
<ha-md-button-menu
.disabled=${this.disabled}
@opening=${this._handleOpen}
@closing=${this._handleClose}
positioning="fixed"
<ha-dropdown
@wa-select=${this._toggleItem}
@wa-show=${this._showDropdown}
placement="bottom"
tabindex="0"
@keydown=${this._handleKeydown}
>
<ha-textfield
<ha-picker-field
slot="trigger"
.label=${this.label}
.value=${data
@ -91,71 +89,42 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
)
.join(", ")}
.disabled=${this.disabled}
tabindex="-1"
></ha-textfield>
<ha-icon-button
slot="trigger"
.label=${this.label}
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-icon-button>
hide-clear-icon
>
</ha-picker-field>
${options.map((item: string | [string, string]) => {
const value = optionValue(item);
const selected = data.includes(value);
return html`<ha-md-menu-item
type="option"
aria-checked=${selected}
return html`<ha-dropdown-item
.value=${value}
.action=${selected ? "remove" : "add"}
.activated=${selected}
@click=${this._toggleItem}
@keydown=${this._keydown}
keep-open
type="checkbox"
.checked=${selected}
>
<ha-checkbox
slot="start"
tabindex="-1"
.checked=${selected}
></ha-checkbox>
${optionLabel(item)}
</ha-md-menu-item>`;
</ha-dropdown-item>`;
})}
</ha-md-button-menu>
</ha-dropdown>
`;
}
protected _keydown(ev) {
if (ev.code === "Space" || ev.code === "Enter") {
ev.preventDefault();
this._toggleItem(ev);
}
}
protected _toggleItem(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.preventDefault(); // keep the dropdown open
const value = ev.detail.item.value;
const action = (ev.detail.item as any).action;
protected _toggleItem(ev) {
const oldData = this.data || [];
let newData: string[];
if (ev.currentTarget.action === "add") {
newData = [...oldData, ev.currentTarget.value];
if (action === "add") {
newData = [...oldData, value];
} else {
newData = oldData.filter((d) => d !== ev.currentTarget.value);
newData = oldData.filter((d) => d !== value);
}
fireEvent(this, "value-changed", {
value: newData,
});
}
protected firstUpdated() {
this.updateComplete.then(() => {
const { formElement, mdcRoot } =
this.shadowRoot?.querySelector("ha-textfield") || ({} as any);
if (formElement) {
formElement.style.textOverflow = "ellipsis";
}
if (mdcRoot) {
mdcRoot.style.cursor = "pointer";
}
});
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("schema")) {
this.toggleAttribute(
@ -194,25 +163,28 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
});
}
private _handleOpen(ev: Event): void {
ev.stopPropagation();
this._opened = true;
this.toggleAttribute("opened", true);
private _showDropdown(ev) {
if (this.disabled) {
ev.preventDefault();
}
this.style.setProperty(
"--dropdown-width",
`${this._dropdown.offsetWidth}px`
);
}
private _handleClose(ev: Event): void {
ev.stopPropagation();
this._opened = false;
this.toggleAttribute("opened", false);
private _handleKeydown(ev) {
if ((ev.code === "Space" || ev.code === "Enter") && this._dropdown) {
this._dropdown.open = true;
}
}
static styles = css`
:host([own-margin]) {
margin-bottom: 5px;
}
ha-md-button-menu {
ha-dropdown {
display: block;
cursor: pointer;
}
ha-formfield {
display: block;
@ -239,9 +211,15 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
:host([opened]) ha-icon-button {
color: var(--primary-color);
}
:host([opened]) ha-md-button-menu {
--mdc-text-field-idle-line-color: var(--input-hover-line-color);
--mdc-text-field-label-ink-color: var(--primary-color);
ha-dropdown::part(menu) {
border-top-left-radius: 0;
border-top-right-radius: 0;
width: var(--dropdown-width);
}
:host([disabled]) ha-dropdown ha-picker-field {
cursor: not-allowed;
}
`;
}

View File

@ -1,123 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
import type { HaButton } from "./ha-button";
import type { HaIconButton } from "./ha-icon-button";
import "./ha-md-menu";
import type { HaMdMenu } from "./ha-md-menu";
@customElement("ha-md-button-menu")
export class HaMdButtonMenu extends LitElement {
protected readonly [FOCUS_TARGET];
@property({ type: Boolean }) public disabled = false;
@property() public positioning?: "fixed" | "absolute" | "popover";
@property({ attribute: "anchor-corner" }) public anchorCorner:
| "start-start"
| "start-end"
| "end-start"
| "end-end" = "end-start";
@property({ attribute: "menu-corner" }) public menuCorner:
| "start-start"
| "start-end"
| "end-start"
| "end-end" = "start-start";
@property({ type: Boolean, attribute: "has-overflow" }) public hasOverflow =
false;
@property({ type: Boolean }) public quick = false;
@query("ha-md-menu", true) private _menu!: HaMdMenu;
public get items() {
return this._menu.items;
}
public override focus() {
if (this._menu.open) {
this._menu.focus();
} else {
this._triggerButton?.focus();
}
}
protected render(): TemplateResult {
return html`
<div @click=${this._handleClick}>
<slot name="trigger" @slotchange=${this._setTriggerAria}></slot>
</div>
<ha-md-menu
.quick=${this.quick}
.positioning=${this.positioning}
.hasOverflow=${this.hasOverflow}
.anchorCorner=${this.anchorCorner}
.menuCorner=${this.menuCorner}
@opening=${this._handleOpening}
@closing=${this._handleClosing}
>
<slot></slot>
</ha-md-menu>
`;
}
private _handleOpening(): void {
fireEvent(this, "opening", undefined, { composed: false });
}
private _handleClosing(): void {
fireEvent(this, "closing", undefined, { composed: false });
}
private _handleClick(): void {
if (this.disabled) {
return;
}
this._menu.anchorElement = this;
if (this._menu.open) {
this._menu.close();
} else {
this._menu.show();
}
}
private get _triggerButton() {
return this.querySelector(
'ha-icon-button[slot="trigger"], ha-button[slot="trigger"], ha-assist-chip[slot="trigger"]'
) as HaIconButton | HaButton | null;
}
private _setTriggerAria() {
if (this._triggerButton) {
this._triggerButton.ariaHasPopup = "menu";
}
}
static styles = css`
:host {
display: inline-block;
position: relative;
}
::slotted([disabled]) {
color: var(--disabled-text-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-md-button-menu": HaMdButtonMenu;
}
}
declare global {
interface HASSDomEvents {
opening: undefined;
closing: undefined;
}
}

View File

@ -8,8 +8,10 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { formatLanguageCode } from "../../common/language/format_language";
import "../../components/chips/ha-assist-chip";
import "../../components/ha-dialog";
import "../../components/ha-dropdown";
import "../../components/ha-dropdown-item";
import type { HaDropdownItem } from "../../components/ha-dropdown-item";
import { getLanguageOptions } from "../../components/ha-language-picker";
import "../../components/ha-md-button-menu";
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
import { fetchAssistSatelliteConfiguration } from "../../data/assist_satellite";
import { getLanguageScores } from "../../data/conversation";
@ -169,9 +171,9 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
>`
: this._step === STEP.PIPELINE
? this._language
? html`<ha-md-button-menu
? html`<ha-dropdown
slot="actionItems"
positioning="fixed"
@wa-select=${this._handlePickLanguage}
>
<ha-assist-chip
.label=${formatLanguageCode(
@ -192,16 +194,14 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
this.hass.locale
).map(
(lang) =>
html`<ha-md-menu-item
html`<ha-dropdown-item
.value=${lang.id}
@click=${this._handlePickLanguage}
@keydown=${this._handlePickLanguage}
.selected=${this._language === lang.id}
class=${this._language === lang.id ? "selected" : ""}
>
${lang.primary}
</ha-md-menu-item>`
</ha-dropdown-item>`
)}
</ha-md-button-menu>`
</ha-dropdown>`
: nothing
: nothing}
</ha-dialog-header>
@ -328,10 +328,8 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
}
}
private _handlePickLanguage(ev) {
if (ev.type === "keydown" && ev.key !== "Enter" && ev.key !== " ") return;
this._language = ev.target.value;
private _handlePickLanguage(ev: CustomEvent<{ item: HaDropdownItem }>) {
this._language = ev.detail.item.value;
}
private _languageChanged(ev: CustomEvent) {
@ -401,7 +399,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
margin: 24px;
display: block;
}
ha-md-button-menu {
ha-dropdown {
height: 48px;
display: flex;
align-items: center;
@ -409,6 +407,13 @@ export class HaVoiceAssistantSetupDialog extends LitElement {
margin-inline-end: 12px;
margin-inline-start: initial;
}
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

@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { firstWeekdayIndex } from "../../../../../common/datetime/first_weekday";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
@ -11,7 +12,6 @@ import type { TimeTrigger } from "../../../../../data/automation";
import type { FrontendLocaleData } from "../../../../../data/translation";
import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row";
import { computeDomain } from "../../../../../common/entity/compute_domain";
const MODE_TIME = "time";
const MODE_ENTITY = "entity";

View File

@ -20,7 +20,6 @@ import "../../components/ha-dropdown-item";
import "../../components/ha-icon-button";
import "../../components/ha-label";
import "../../components/ha-list-item";
import "../../components/ha-md-button-menu";
import "../../components/ha-md-menu-item";
import "../../components/ha-settings-row";
import { deleteAllRefreshTokens } from "../../data/auth";