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

View File

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

View File

@ -2,16 +2,11 @@ import { mdiAlertOctagram, mdiCheckBold } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../ha-button";
import type { Appearance } from "../ha-button";
import "../ha-spinner";
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")
export class HaProgressButton extends LitElement {
@ -23,18 +18,16 @@ export class HaProgressButton extends LitElement {
@property() appearance: Appearance = "accent";
@property({ attribute: false }) public iconPath?: string;
@property() variant: "brand" | "danger" | "neutral" | "warning" | "success" =
"brand";
@state() private _result?: "success" | "error";
@state() private _hasInitialIcon = false;
public render(): TemplateResult {
const appearance =
this.progress || this._result
? HIGHLIGHT_APPEARANCE[this.appearance]
: this.appearance;
this.progress || this._result ? "accent" : this.appearance;
return html`
<ha-button
@ -46,20 +39,20 @@ export class HaProgressButton extends LitElement {
: this._result === "error"
? "danger"
: this.variant}
.hideContent=${this._result !== undefined}
class=${classMap({
result: !!this._result,
success: this._result === "success",
error: this._result === "error",
})}
>
${this._hasInitialIcon
? html`<slot name="icon" slot="start"></slot>`
${this.iconPath
? html`<ha-svg-icon
.path=${this.iconPath}
slot="start"
></ha-svg-icon>`
: nothing}
<slot
>${this._result
? html`<ha-svg-icon
.path=${this._result === "success"
? mdiCheckBold
: mdiAlertOctagram}
></ha-svg-icon>`
: this.label}</slot
>
<slot>${this.label}</slot>
</ha-button>
${!this._result
? 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 {
this._setResult("success");
}
@ -109,10 +94,6 @@ export class HaProgressButton extends LitElement {
pointer-events: none;
}
ha-button {
transition: all 1s;
}
.progress {
bottom: 0;
display: flex;
@ -123,14 +104,21 @@ export class HaProgressButton extends LitElement {
width: 100%;
}
ha-svg-icon {
color: white;
ha-button {
width: 100%;
}
ha-button.success slot,
ha-button.error slot {
ha-button.result::part(start),
ha-button.result::part(end),
ha-button.result::part(label),
ha-button.result::part(caret),
ha-button.result::part(spinner) {
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 { css, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement } from "lit/decorators";
export type Appearance = "accent" | "filled" | "outlined" | "plain";
@ -36,9 +36,6 @@ export type Appearance = "accent" | "filled" | "outlined" | "plain";
export class HaButton extends Button {
variant: "brand" | "neutral" | "success" | "warning" | "danger" = "brand";
@property({ type: Boolean, attribute: "hide-content", reflect: true })
hideContent = false;
static get styles(): CSSResultGroup {
return [
Button.styles,
@ -150,6 +147,10 @@ export class HaButton extends Button {
background-color: var(--color-fill-disabled-loud-resting);
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 { storage } from "../../common/decorators/storage";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/buttons/ha-progress-button";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../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 { showAlertDialog } from "../generic/show-dialog-box";
import type { TTSTryDialogParams } from "./show-dialog-tts-try";
import "../../components/buttons/ha-progress-button";
@customElement("dialog-tts-try")
export class TTSTryDialog extends LitElement {
@ -85,11 +85,11 @@ export class TTSTryDialog extends LitElement {
.progress=${this._loadingExample}
?dialogInitialFocus=${Boolean(this._defaultMessage)}
slot="primaryAction"
.label=${this.hass.localize("ui.dialogs.tts-try.play")}
@click=${this._playExample}
.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-dialog>
`;

View File

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

View File

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