Migrate ha-button-toggle-group to webawesome (#26506)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Wendelin
2025-08-14 14:52:39 +02:00
committed by GitHub
parent f133f246cb
commit 208fd0662c
8 changed files with 178 additions and 133 deletions

View File

@@ -61,7 +61,6 @@
"@material/chips": "=14.0.0-canary.53b3cad2f.0", "@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0", "@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0", "@material/mwc-base": "0.27.0",
"@material/mwc-button": "0.27.0",
"@material/mwc-checkbox": "0.27.0", "@material/mwc-checkbox": "0.27.0",
"@material/mwc-dialog": "0.27.0", "@material/mwc-dialog": "0.27.0",
"@material/mwc-drawer": "0.27.0", "@material/mwc-drawer": "0.27.0",

View File

@@ -0,0 +1,82 @@
import ButtonGroup from "@awesome.me/webawesome/dist/components/button-group/button-group";
import { customElement } from "lit/decorators";
import type { HaButton } from "./ha-button";
import { StateSet } from "../resources/polyfills/stateset";
export type Appearance = "accent" | "filled" | "outlined" | "plain";
/**
* Finds an ha-button element either as the current element or within its descendants.
* @param el - The HTML element to search from
* @returns The found HaButton element, or null if not found
*/
function findButton(el: HTMLElement) {
const selector = "ha-button";
return (el.closest(selector) ?? el.querySelector(selector)) as HaButton;
}
/**
* @element ha-button-group
* @extends {ButtonGroup}
* @summary
* Group buttons. Extend Webawesome to be able to work with ha-button tags
*
* @documentation https://webawesome.com/components/button-group
*/
@customElement("ha-button-group") // @ts-expect-error Intentionally overriding private methods
export class HaButtonGroup extends ButtonGroup {
attachInternals() {
const internals = super.attachInternals();
Object.defineProperty(internals, "states", {
value: new StateSet(this, internals.states),
});
return internals;
}
// @ts-expect-error updateClassNames is used in super class
// eslint-disable-next-line @typescript-eslint/naming-convention
private override updateClassNames() {
const slottedElements = [
...this.defaultSlot.assignedElements({ flatten: true }),
] as HTMLElement[];
this.hasOutlined = false;
slottedElements.forEach((el) => {
const index = slottedElements.indexOf(el);
const button = findButton(el);
if (button) {
if ((button as HaButton).appearance === "outlined")
this.hasOutlined = true;
if (this.size) button.setAttribute("size", this.size);
button.classList.add("wa-button-group__button");
button.classList.toggle(
"wa-button-group__horizontal",
this.orientation === "horizontal"
);
button.classList.toggle(
"wa-button-group__vertical",
this.orientation === "vertical"
);
button.classList.toggle("wa-button-group__button-first", index === 0);
button.classList.toggle(
"wa-button-group__button-inner",
index > 0 && index < slottedElements.length - 1
);
button.classList.toggle(
"wa-button-group__button-last",
index === slottedElements.length - 1
);
// use button-group variant
button.setAttribute("variant", this.variant);
}
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-button-group": HaButtonGroup;
}
}

View File

@@ -1,141 +1,72 @@
import "@material/mwc-button/mwc-button";
import type { Button } from "@material/mwc-button/mwc-button";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property, queryAll } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { ToggleButton } from "../types"; import type { ToggleButton } from "../types";
import "./ha-icon-button"; import "./ha-svg-icon";
import "./ha-button";
import "./ha-button-group";
/**
* @element ha-button-toggle-group
*
* @summary
* A button-group with one active selection.
*
* @attr {ToggleButton[]} buttons - the button config
* @attr {string} active - The value of the currently active button.
* @attr {("small"|"medium")} size - The size of the buttons in the group.
* @attr {("brand"|"neutral"|"success"|"warning"|"danger")} variant - The variant of the buttons in the group.
*
* @fires value-changed - Dispatched when the active button changes.
*/
@customElement("ha-button-toggle-group") @customElement("ha-button-toggle-group")
export class HaButtonToggleGroup extends LitElement { export class HaButtonToggleGroup extends LitElement {
@property({ attribute: false }) public buttons!: ToggleButton[]; @property({ attribute: false }) public buttons!: ToggleButton[];
@property() public active?: string; @property() public active?: string;
@property({ attribute: "full-width", type: Boolean }) @property({ reflect: true }) size: "small" | "medium" = "medium";
public fullWidth = false;
@property({ type: Boolean }) public dense = false; @property() public variant:
| "brand"
@queryAll("mwc-button") private _buttons?: Button[]; | "neutral"
| "success"
| "warning"
| "danger" = "brand";
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div> <ha-button-group .variant=${this.variant} .size=${this.size}>
${this.buttons.map((button) => ${this.buttons.map(
button.iconPath (button) =>
? html`<ha-icon-button html`<ha-button
.label=${button.label} class="icon"
.path=${button.iconPath} .value=${button.value}
.value=${button.value} @click=${this._handleClick}
?active=${this.active === button.value} .title=${button.label}
@click=${this._handleClick} .appearance=${this.active === button.value ? "accent" : "filled"}
></ha-icon-button>` >
: html`<mwc-button ${button.iconPath
style=${styleMap({ ? html`<ha-svg-icon
width: this.fullWidth aria-label=${button.label}
? `${100 / this.buttons.length}%` .path=${button.iconPath}
: "initial", ></ha-svg-icon>`
})} : button.label}
outlined </ha-button>`
.dense=${this.dense}
.value=${button.value}
?active=${this.active === button.value}
@click=${this._handleClick}
>${button.label}</mwc-button
>`
)} )}
</div> </ha-button-group>
`; `;
} }
protected updated() {
// Work around Safari default margin that is not reset in mwc-button as of aug 2021
this._buttons?.forEach(async (button) => {
await button.updateComplete;
(
button.shadowRoot!.querySelector("button") as HTMLButtonElement
).style.margin = "0";
});
}
private _handleClick(ev): void { private _handleClick(ev): void {
this.active = ev.currentTarget.value; this.active = ev.currentTarget.value;
fireEvent(this, "value-changed", { value: this.active }); fireEvent(this, "value-changed", { value: this.active });
} }
static styles = css` static styles = css`
div { :host {
display: flex;
--mdc-icon-button-size: var(--button-toggle-size, 36px);
--mdc-icon-size: var(--button-toggle-icon-size, 20px); --mdc-icon-size: var(--button-toggle-icon-size, 20px);
direction: ltr;
}
mwc-button {
flex: 1;
--mdc-shape-small: 0;
--mdc-button-outline-width: 1px 0 1px 1px;
--mdc-button-outline-color: var(--primary-color);
}
ha-icon-button {
border: 1px solid var(--primary-color);
border-right-width: 0px;
}
ha-icon-button,
mwc-button {
position: relative;
cursor: pointer;
}
ha-icon-button::before,
mwc-button::before {
top: 0;
left: 0;
width: 100%;
height: 100%;
position: absolute;
background-color: var(--primary-color);
opacity: 0;
pointer-events: none;
content: "";
transition:
opacity 15ms linear,
background-color 15ms linear;
}
ha-icon-button[active]::before,
mwc-button[active]::before {
opacity: 1;
}
ha-icon-button[active] {
--icon-primary-color: var(--text-primary-color);
}
mwc-button[active] {
--mdc-theme-primary: var(--text-primary-color);
}
ha-icon-button:first-child,
mwc-button:first-child {
--mdc-shape-small: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
--mdc-button-outline-width: 1px;
}
mwc-button:first-child::before {
border-radius: 4px 0 0 4px;
}
ha-icon-button:last-child,
mwc-button:last-child {
border-radius: 0 4px 4px 0;
border-right-width: 1px;
--mdc-shape-small: 0 4px 4px 0;
--mdc-button-outline-width: 1px;
}
mwc-button:last-child::before {
border-radius: 0 4px 4px 0;
}
ha-icon-button:only-child,
mwc-button:only-child {
--mdc-shape-small: 4px;
border-right-width: 1px;
} }
`; `;
} }

View File

@@ -35,7 +35,7 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
* @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click. * @attr {boolean} loading - shows a loading indicator instead of the buttons label and disable buttons click.
* @attr {boolean} disabled - Disables the button and prevents user interaction. * @attr {boolean} disabled - Disables the button and prevents user interaction.
*/ */
@customElement("ha-button") @customElement("ha-button") // @ts-expect-error Intentionally overriding private methods
export class HaButton extends Button { export class HaButton extends Button {
variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand"; variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand";
@@ -47,6 +47,42 @@ export class HaButton extends Button {
return internals; return internals;
} }
// @ts-expect-error handleLabelSlotChange is used in super class
// eslint-disable-next-line @typescript-eslint/naming-convention
private override handleLabelSlotChange() {
const nodes = this.labelSlot.assignedNodes({ flatten: true });
let hasIconLabel = false;
let hasIcon = false;
let text = "";
// If there's only an icon and no text, it's an icon button
[...nodes].forEach((node) => {
if (
node.nodeType === Node.ELEMENT_NODE &&
(node as HTMLElement).localName === "ha-svg-icon"
) {
hasIcon = true;
if (!hasIconLabel)
hasIconLabel = (node as HTMLElement).hasAttribute("aria-label");
}
// Concatenate text nodes
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent;
}
});
this.isIconButton = text.trim() === "" && hasIcon;
if (__DEV__ && this.isIconButton && !hasIconLabel) {
// eslint-disable-next-line no-console
console.warn(
'Icon buttons must have a label for screen readers. Add <ha-svg-icon aria-label="..."> to remove this warning.',
this
);
}
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
Button.styles, Button.styles,
@@ -181,6 +217,11 @@ export class HaButton extends Button {
color: var(--wa-color-on-normal); color: var(--wa-color-on-normal);
} }
} }
:host([appearance~="filled"]) .button {
color: var(--wa-color-on-normal);
background-color: var(--wa-color-fill-normal);
border-color: transparent;
}
:host([appearance~="filled"]) :host([appearance~="filled"])
.button:not(.disabled):not(.loading):active { .button:not(.disabled):not(.loading):active {
background-color: var(--button-color-fill-normal-active); background-color: var(--button-color-fill-normal-active);

View File

@@ -385,30 +385,22 @@ export class HAFullCalendar extends LitElement {
if (!this._viewButtons) { if (!this._viewButtons) {
this._viewButtons = [ this._viewButtons = [
{ {
label: localize( label: localize("ui.components.calendar.views.dayGridMonth"),
"ui.panel.lovelace.editor.card.calendar.views.dayGridMonth"
),
value: "dayGridMonth", value: "dayGridMonth",
iconPath: mdiViewModule, iconPath: mdiViewModule,
}, },
{ {
label: localize( label: localize("ui.components.calendar.views.dayGridWeek"),
"ui.panel.lovelace.editor.card.calendar.views.dayGridWeek"
),
value: "dayGridWeek", value: "dayGridWeek",
iconPath: mdiViewWeek, iconPath: mdiViewWeek,
}, },
{ {
label: localize( label: localize("ui.components.calendar.views.dayGridDay"),
"ui.panel.lovelace.editor.card.calendar.views.dayGridDay"
),
value: "dayGridDay", value: "dayGridDay",
iconPath: mdiViewDay, iconPath: mdiViewDay,
}, },
{ {
label: localize( label: localize("ui.components.calendar.views.listWeek"),
"ui.panel.lovelace.editor.card.calendar.views.listWeek"
),
value: "listWeek", value: "listWeek",
iconPath: mdiViewAgenda, iconPath: mdiViewAgenda,
}, },
@@ -493,10 +485,6 @@ export class HAFullCalendar extends LitElement {
--mdc-icon-button-size: 32px; --mdc-icon-button-size: 32px;
} }
ha-button-toggle-group {
color: var(--primary-color);
}
ha-fab { ha-fab {
position: absolute; position: absolute;
bottom: 16px; bottom: 16px;

View File

@@ -1,4 +1,3 @@
import "@material/mwc-button";
import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js"; import { mdiHelpCircle, mdiStarFourPoints } from "@mdi/js";
import { css, html, LitElement } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";

View File

@@ -1168,6 +1168,12 @@
}, },
"summary": "Summary", "summary": "Summary",
"description": "Description" "description": "Description"
},
"views": {
"dayGridMonth": "[%key:ui::panel::lovelace::editor::card::calendar::views::dayGridMonth%]",
"dayGridWeek": "[%key:ui::panel::lovelace::editor::card::calendar::views::dayGridWeek%]",
"dayGridDay": "[%key:ui::panel::lovelace::editor::card::calendar::views::dayGridDay%]",
"listWeek": "[%key:ui::panel::lovelace::editor::card::calendar::views::listWeek%]"
} }
}, },
"attributes": { "attributes": {

View File

@@ -2666,7 +2666,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@material/mwc-button@npm:0.27.0, @material/mwc-button@npm:^0.27.0": "@material/mwc-button@npm:^0.27.0":
version: 0.27.0 version: 0.27.0
resolution: "@material/mwc-button@npm:0.27.0" resolution: "@material/mwc-button@npm:0.27.0"
dependencies: dependencies:
@@ -9383,7 +9383,6 @@ __metadata:
"@material/chips": "npm:=14.0.0-canary.53b3cad2f.0" "@material/chips": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0" "@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/mwc-base": "npm:0.27.0" "@material/mwc-base": "npm:0.27.0"
"@material/mwc-button": "npm:0.27.0"
"@material/mwc-checkbox": "npm:0.27.0" "@material/mwc-checkbox": "npm:0.27.0"
"@material/mwc-dialog": "npm:0.27.0" "@material/mwc-dialog": "npm:0.27.0"
"@material/mwc-drawer": "npm:0.27.0" "@material/mwc-drawer": "npm:0.27.0"