Fix ha-progress-button

This commit is contained in:
Wendelin 2025-07-28 17:35:06 +02:00
parent 39171c95d9
commit 7ce88da20e
No known key found for this signature in database
9 changed files with 211 additions and 62 deletions

View File

@ -0,0 +1,32 @@
---
title: Progress Button
---
<style>
.wrapper {
display: flex;
gap: 24px;
}
</style>
# Progress Button `<ha-progress-button>`
### API
This component is a wrapper around `<ha-button>` that adds support for showing progress
**Slots**
- default slot: Label of the button
` - no default
**Properties/Attributes**
| Name | Type | Default | Description |
| ---------- | ---------------------------------------------- | --------- | -------------------------------------------------- |
| label | string | "accent" | Sets the button label. |
| disabled | Boolean | false | Disables the button if true. |
| progress | Boolean | false | Shows a progress indicator on the button. |
| appearance | "accent"/"filled"/"plain" | "accent" | Sets the button appearance. |
| variants | "brand"/"danger"/"neutral"/"warning"/"success" | "brand" | Sets the button color variant. "brand" is default. |
| iconPath | string | undefined | Sets the icon path for the button. |

View File

@ -0,0 +1,139 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import { mdiHomeAssistant } from "../../../../src/resources/home-assistant-logo-svg";
@customElement("demo-components-ha-progress-button")
export class DemoHaProgressButton extends LitElement {
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-progress-button in ${mode}">
<div class="card-content">
<ha-progress-button @click=${this._clickedSuccess}>
Success
</ha-progress-button>
<ha-progress-button @click=${this._clickedFail}>
Fail
</ha-progress-button>
<ha-progress-button size="small" @click=${this._clickedSuccess}>
small
</ha-progress-button>
<ha-progress-button
appearance="filled"
@click=${this._clickedSuccess}
>
filled
</ha-progress-button>
<ha-progress-button
appearance="plain"
@click=${this._clickedSuccess}
>
plain
</ha-progress-button>
<ha-progress-button
variant="warning"
@click=${this._clickedSuccess}
>
warning
</ha-progress-button>
<ha-progress-button
variant="neutral"
@click=${this._clickedSuccess}
label="with icon"
.iconPath=${mdiHomeAssistant}
>
With Icon
</ha-progress-button>
<ha-progress-button progress @click=${this._clickedSuccess}>
progress
</ha-progress-button>
<ha-progress-button disabled @click=${this._clickedSuccess}>
disabled
</ha-progress-button>
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
private async _clickedSuccess(ev: CustomEvent): Promise<void> {
console.log("Clicked success");
const button = ev.currentTarget as any;
button.progress = true;
setTimeout(() => {
button.actionSuccess();
button.progress = false;
}, 1000);
}
private async _clickedFail(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
setTimeout(() => {
button.actionError();
button.progress = false;
}, 1000);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
.button {
padding: unset;
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.card-content div {
display: flex;
gap: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-progress-button": DemoHaProgressButton;
}
}

View File

@ -3,8 +3,8 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { atLeastVersion } from "../../../src/common/config/version"; import { atLeastVersion } from "../../../src/common/config/version";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-button"; import "../../../src/components/ha-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import type { HassioStats } from "../../../src/data/hassio/common"; import type { HassioStats } from "../../../src/data/hassio/common";
@ -94,7 +94,7 @@ class HassioCoreInfo extends LitElement {
<div class="card-actions"> <div class="card-actions">
<ha-progress-button <ha-progress-button
slot="primaryAction" slot="primaryAction"
class="warning" variant="danger"
@click=${this._coreRestart} @click=${this._coreRestart}
.title=${this.supervisor.localize("common.restart_name", { .title=${this.supervisor.localize("common.restart_name", {
name: "Core", name: "Core",
@ -187,11 +187,6 @@ class HassioCoreInfo extends LitElement {
white-space: normal; white-space: normal;
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.warning {
--mdc-theme-primary: var(--error-color);
}
ha-button-menu { ha-button-menu {
color: var(--secondary-text-color); color: var(--secondary-text-color);
--mdc-menu-min-width: 200px; --mdc-menu-min-width: 200px;

View File

@ -171,7 +171,7 @@ class HassioHostInfo extends LitElement {
<div class="card-actions"> <div class="card-actions">
${this.supervisor.host.features.includes("reboot") ${this.supervisor.host.features.includes("reboot")
? html` ? html`
<ha-progress-button class="warning" @click=${this._hostReboot}> <ha-progress-button variant="danger" @click=${this._hostReboot}>
${this.supervisor.localize("system.host.reboot_host")} ${this.supervisor.localize("system.host.reboot_host")}
</ha-progress-button> </ha-progress-button>
` `
@ -179,7 +179,7 @@ class HassioHostInfo extends LitElement {
${this.supervisor.host.features.includes("shutdown") ${this.supervisor.host.features.includes("shutdown")
? html` ? html`
<ha-progress-button <ha-progress-button
class="warning" variant="danger"
@click=${this._hostShutdown} @click=${this._hostShutdown}
> >
${this.supervisor.localize("system.host.shutdown_host")} ${this.supervisor.localize("system.host.shutdown_host")}
@ -435,10 +435,6 @@ class HassioHostInfo extends LitElement {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.warning {
--mdc-theme-primary: var(--error-color);
}
ha-button-menu { ha-button-menu {
color: var(--secondary-text-color); color: var(--secondary-text-color);
--mdc-menu-min-width: 200px; --mdc-menu-min-width: 200px;

View File

@ -2,16 +2,11 @@ import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../ha-button"; import "../ha-button";
import type { Appearance } from "../ha-button";
import "../ha-spinner"; import "../ha-spinner";
import "../ha-svg-icon"; import "../ha-svg-icon";
import type { Appearance } from "../ha-button";
const HIGHLIGHT_APPEARANCE = {
accent: "accent" as Appearance,
filled: "accent" as Appearance,
plain: "filled" as Appearance,
};
@customElement("ha-progress-button") @customElement("ha-progress-button")
export class HaProgressButton extends LitElement { export class HaProgressButton extends LitElement {
@ -23,18 +18,16 @@ export class HaProgressButton extends LitElement {
@property() appearance: Appearance = "accent"; @property() appearance: Appearance = "accent";
@property({ attribute: false }) public iconPath?: string;
@property() variant: "brand" | "danger" | "neutral" | "warning" | "success" = @property() variant: "brand" | "danger" | "neutral" | "warning" | "success" =
"brand"; "brand";
@state() private _result?: "success" | "error"; @state() private _result?: "success" | "error";
@state() private _hasInitialIcon = false;
public render(): TemplateResult { public render(): TemplateResult {
const appearance = const appearance =
this.progress || this._result this.progress || this._result ? "accent" : this.appearance;
? HIGHLIGHT_APPEARANCE[this.appearance]
: this.appearance;
return html` return html`
<ha-button <ha-button
@ -46,20 +39,20 @@ export class HaProgressButton extends LitElement {
: this._result === "error" : this._result === "error"
? "danger" ? "danger"
: this.variant} : this.variant}
.hideContent=${this._result !== undefined} class=${classMap({
result: !!this._result,
success: this._result === "success",
error: this._result === "error",
})}
> >
${this._hasInitialIcon ${this.iconPath
? html`<slot name="icon" slot="start"></slot>`
: nothing}
<slot
>${this._result
? html`<ha-svg-icon ? html`<ha-svg-icon
.path=${this._result === "success" .path=${this.iconPath}
? mdiCheckBold slot="start"
: mdiAlertOctagram}
></ha-svg-icon>` ></ha-svg-icon>`
: this.label}</slot : nothing}
>
<slot>${this.label}</slot>
</ha-button> </ha-button>
${!this._result ${!this._result
? nothing ? nothing
@ -75,14 +68,6 @@ export class HaProgressButton extends LitElement {
`; `;
} }
firstUpdated() {
const iconSlot = this.shadowRoot!.querySelector(
'slot[name="icon"]'
) as HTMLSlotElement;
this._hasInitialIcon =
iconSlot && iconSlot.assignedNodes({ flatten: true }).length > 0;
}
public actionSuccess(): void { public actionSuccess(): void {
this._setResult("success"); this._setResult("success");
} }
@ -109,10 +94,6 @@ export class HaProgressButton extends LitElement {
pointer-events: none; pointer-events: none;
} }
ha-button {
transition: all 1s;
}
.progress { .progress {
bottom: 0; bottom: 0;
display: flex; display: flex;
@ -123,14 +104,21 @@ export class HaProgressButton extends LitElement {
width: 100%; width: 100%;
} }
ha-svg-icon { ha-button {
color: white; width: 100%;
} }
ha-button.success slot, ha-button.result::part(start),
ha-button.error slot { ha-button.result::part(end),
ha-button.result::part(label),
ha-button.result::part(caret),
ha-button.result::part(spinner) {
visibility: hidden; visibility: hidden;
} }
ha-svg-icon {
color: var(--white);
}
`; `;
} }

View File

@ -1,6 +1,6 @@
import Button from "@awesome.me/webawesome/dist/components/button/button"; import Button from "@awesome.me/webawesome/dist/components/button/button";
import { css, type CSSResultGroup } from "lit"; import { css, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement } from "lit/decorators";
export type Appearance = "accent" | "filled" | "outlined" | "plain"; export type Appearance = "accent" | "filled" | "outlined" | "plain";
@ -36,9 +36,6 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
export class HaButton extends Button { export class HaButton extends Button {
variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand"; variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand";
@property({ type: Boolean, attribute: "hide-content", reflect: true })
hideContent = false;
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
Button.styles, Button.styles,
@ -150,6 +147,10 @@ export class HaButton extends Button {
background-color: var(--color-fill-disabled-loud-resting); background-color: var(--color-fill-disabled-loud-resting);
color: var(--color-on-disabled-loud); color: var(--color-on-disabled-loud);
} }
:host([loading]) {
pointer-events: none;
}
`, `,
]; ];
} }

View File

@ -3,6 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { storage } from "../../common/decorators/storage"; import { storage } from "../../common/decorators/storage";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import "../../components/buttons/ha-progress-button";
import { createCloseHeading } from "../../components/ha-dialog"; import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-textarea"; import "../../components/ha-textarea";
import type { HaTextArea } from "../../components/ha-textarea"; import type { HaTextArea } from "../../components/ha-textarea";
@ -10,7 +11,6 @@ import { convertTextToSpeech } from "../../data/tts";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box"; import { showAlertDialog } from "../generic/show-dialog-box";
import type { TTSTryDialogParams } from "./show-dialog-tts-try"; import type { TTSTryDialogParams } from "./show-dialog-tts-try";
import "../../components/buttons/ha-progress-button";
@customElement("dialog-tts-try") @customElement("dialog-tts-try")
export class TTSTryDialog extends LitElement { export class TTSTryDialog extends LitElement {
@ -85,11 +85,11 @@ export class TTSTryDialog extends LitElement {
.progress=${this._loadingExample} .progress=${this._loadingExample}
?dialogInitialFocus=${Boolean(this._defaultMessage)} ?dialogInitialFocus=${Boolean(this._defaultMessage)}
slot="primaryAction" slot="primaryAction"
.label=${this.hass.localize("ui.dialogs.tts-try.play")}
@click=${this._playExample} @click=${this._playExample}
.disabled=${!this._valid} .disabled=${!this._valid}
.iconPath=${mdiPlayCircleOutline}
> >
<ha-svg-icon slot="icon" .path=${mdiPlayCircleOutline}></ha-svg-icon> ${this.hass.localize("ui.dialogs.tts-try.play")}
</ha-progress-button> </ha-progress-button>
</ha-dialog> </ha-dialog>
`; `;

View File

@ -114,7 +114,6 @@ export class CloudLogin extends LitElement {
)} )}
</ha-button> </ha-button>
<ha-progress-button <ha-progress-button
unelevated
@click=${this._handleLogin} @click=${this._handleLogin}
.progress=${this._inProgress} .progress=${this._inProgress}
>${this.localize( >${this.localize(

View File

@ -8,7 +8,6 @@ import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { round } from "../../../common/number/round"; import { round } from "../../../common/number/round";
import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
import "../../../components/buttons/ha-progress-button";
import "../../../components/chart/ha-chart-base"; import "../../../components/chart/ha-chart-base";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-button"; import "../../../components/ha-button";