Skip to content

feat(ui5-button): Add BusyIndicator #11906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 32 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0e0d4d1
feat: add busy indicator in ui5-button
GDamyanov Jul 11, 2025
3a26280
feat: add opacity background to buttom when loading
GDamyanov Jul 14, 2025
79e70aa
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 14, 2025
f14d013
feat: stype opacity on busy indicator in button
GDamyanov Jul 14, 2025
b805686
Merge branch 'button-busy-indicator' of https://github.com/SAP/ui5-we…
GDamyanov Jul 14, 2025
b9964b0
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 14, 2025
1962be2
refactor: remove tabs
GDamyanov Jul 14, 2025
b692a21
Merge branch 'button-busy-indicator' of https://github.com/SAP/ui5-we…
GDamyanov Jul 14, 2025
7b6b898
refactor: remove tabs
GDamyanov Jul 14, 2025
ae4bc15
refactor: update text
GDamyanov Jul 14, 2025
1475f14
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 15, 2025
3ef3b12
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 15, 2025
bdf1e56
Update packages/main/src/Button.ts
GDamyanov Jul 16, 2025
6526dca
fix: remove redundant tooltip
GDamyanov Jul 16, 2025
b386727
feat: add loadingDelay property
GDamyanov Jul 16, 2025
5c0344e
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 16, 2025
54636c4
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 17, 2025
1258347
refactor: update jsdoc
GDamyanov Jul 17, 2025
420238c
Merge branch 'button-busy-indicator' of https://github.com/SAP/ui5-we…
GDamyanov Jul 17, 2025
44a4d02
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 17, 2025
bfd6ae1
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 18, 2025
c8dc14d
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 21, 2025
a4c0ab1
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 21, 2025
7b1d828
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 21, 2025
af1109d
feat: add sample in playground
GDamyanov Jul 21, 2025
2d4d825
Merge branch 'button-busy-indicator' of https://github.com/SAP/ui5-we…
GDamyanov Jul 21, 2025
96ae6d5
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 21, 2025
17fd6ba
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 21, 2025
cc36fbb
fix: update jsdocs
GDamyanov Jul 21, 2025
8de3c62
Merge branch 'button-busy-indicator' of https://github.com/SAP/ui5-we…
GDamyanov Jul 21, 2025
9c51de8
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 22, 2025
0cf7af8
Merge branch 'main' into button-busy-indicator
GDamyanov Jul 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions packages/main/cypress/specs/Button.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,29 @@ describe("Button general interaction", () => {
.should("not.called");
});

it("tests clicking on button with busy indicator", () => {
cy.mount(<Button loading></Button>);

cy.get("[ui5-button]")
.as("button");

cy.get("@button")
.then(button => {
button.get(0).addEventListener("click", cy.stub().as("clicked"));
});

cy.get("@button")
.shadow()
.find("[ui5-busy-indicator]")
.should("be.visible");

cy.get("@button")
.realClick();

cy.get("@clicked")
.should("not.called");
});

it("tests button with text icon role", () => {
cy.mount(<Button design="Attention" icon="message-warning">Warning</Button>);

Expand Down
38 changes: 32 additions & 6 deletions packages/main/src/Button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,28 @@ class Button extends UI5Element implements IButton {
nonInteractive = false;

/**
* The current title of the button, either the tooltip property or the icons tooltip. The tooltip property with higher prio.
* Defines whether the button shows a loading indicator.
*
* **Note:** If set to `true`, a busy indicator component will be displayed on the related button.
* @default false
* @public
* @since 2.13.0
*/
@property({ type: Boolean })
loading = false;

/**
* Specifies the delay in milliseconds before the loading indicator appears within the associated button.
* @default 1000
* @public
* @since 2.13.0
*/
@property({ type: Number })
loadingDelay = 1000;

/**
* The button's current title is determined by either the `tooltip` property or the icon's tooltip, with the `tooltip`
* property taking precedence if both are set.
* @private
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider paraphrasing:

The button's current title is determined by either the tooltip property or the icon's tooltip, with the tooltip property taking precedence if both are set.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cc36fbb

*/
@property({ noAttribute: true })
Expand Down Expand Up @@ -448,6 +469,11 @@ class Button extends UI5Element implements IButton {
return;
}

if (this.loading) {
e.preventDefault();
return;
}

const {
altKey,
ctrlKey,
Expand Down Expand Up @@ -482,7 +508,7 @@ class Button extends UI5Element implements IButton {
}

_onmousedown() {
if (this.nonInteractive) {
if (this.nonInteractive || this.loading) {
return;
}

Expand All @@ -491,7 +517,7 @@ class Button extends UI5Element implements IButton {
}

_ontouchend(e: TouchEvent) {
if (this.disabled) {
if (this.disabled || this.loading) {
e.preventDefault();
e.stopPropagation();
}
Expand All @@ -506,7 +532,7 @@ class Button extends UI5Element implements IButton {
}

_onkeydown(e: KeyboardEvent) {
this._cancelAction = isShift(e) || isEscape(e);
this._cancelAction = isShift(e) || isEscape(e) || this.loading;

if (isSpace(e) || isEnter(e)) {
this._setActiveState(true);
Expand All @@ -528,7 +554,7 @@ class Button extends UI5Element implements IButton {
}

_onfocusout() {
if (this.nonInteractive) {
if (this.nonInteractive || this.loading) {
return;
}

Expand All @@ -540,7 +566,7 @@ class Button extends UI5Element implements IButton {
_setActiveState(active: boolean) {
const eventPrevented = !this.fireDecoratorEvent("active-state-change");

if (eventPrevented) {
if (eventPrevented || this.loading) {
return;
}

Expand Down
10 changes: 9 additions & 1 deletion packages/main/src/ButtonTemplate.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type Button from "./Button.js";
import Icon from "./Icon.js";
import BusyIndicator from "./BusyIndicator.js";

export default function ButtonTemplate(this: Button, injectedProps?: {
ariaPressed?: boolean,
Expand All @@ -15,7 +16,7 @@ export default function ButtonTemplate(this: Button, injectedProps?: {
"ui5-button-root": true,
"ui5-button-badge-placement-end": this.badge[0]?.design === "InlineText",
"ui5-button-badge-placement-end-top": this.badge[0]?.design === "OverlayText",
"ui5-button-badge-dot": this.badge[0]?.design === "AttentionDot",
"ui5-button-badge-dot": this.badge[0]?.design === "AttentionDot"
}}
disabled={this.disabled}
data-sap-focus-ref
Expand Down Expand Up @@ -69,5 +70,12 @@ export default function ButtonTemplate(this: Button, injectedProps?: {
<slot name="badge"/>
}
</button>
{this.loading &&
<BusyIndicator id={`${this._id}-button-busy-indicator`}
class="ui5-button-busy-indicator"
size={this.iconOnly ? "S" : "M"}
active={true}
delay={this.loadingDelay}/>
}
</>);
}
88 changes: 64 additions & 24 deletions packages/main/src/themes/Button.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
user-select: none;
}

:host(:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host(:not([hidden]):not([disabled]).ui5_hovered) {
:host(:not([active]):not([loading]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host(:not([hidden]):not([loading]):not([disabled]).ui5_hovered) {
background: var(--sapButton_Hover_Background);
border: 1px solid var(--sapButton_Hover_BorderColor);
color: var(--sapButton_Hover_TextColor);
Expand Down Expand Up @@ -108,12 +108,12 @@
pointer-events: none;
}

:host([desktop]:not([active])) .ui5-button-root:focus-within:after,
:host(:not([active])) .ui5-button-root:focus-visible:after,
:host([desktop][active][design="Emphasized"]) .ui5-button-root:focus-within:after,
:host([active][design="Emphasized"]) .ui5-button-root:focus-visible:after,
:host([desktop][active]) .ui5-button-root:focus-within:before,
:host([active]) .ui5-button-root:focus-visible:before {
:host([desktop]:not([active]):not([loading])) .ui5-button-root:focus-within:after,
:host(:not([active]):not([loading])) .ui5-button-root:focus-visible:after,
:host([desktop][active][design="Emphasized"]:not([loading])) .ui5-button-root:focus-within:after,
:host([active][design="Emphasized"]:not([loading])) .ui5-button-root:focus-visible:after,
:host([desktop][active]:not([loading])) .ui5-button-root:focus-within:before,
:host([active]:not([loading])) .ui5-button-root:focus-visible:before {
content: "";
position: absolute;
box-sizing: border-box;
Expand All @@ -125,18 +125,18 @@
border-radius: var(--_ui5_button_focused_border_radius);
}

:host([desktop][active]) .ui5-button-root:focus-within:before,
:host([active]) .ui5-button-root:focus-visible:before {
:host([desktop][active]:not([loading])) .ui5-button-root:focus-within:before,
:host([active]:not([loading])) .ui5-button-root:focus-visible:before {
border-color: var(--_ui5_button_pressed_focused_border_color);
}

:host([design="Emphasized"][desktop]) .ui5-button-root:focus-within:after,
:host([design="Emphasized"]) .ui5-button-root:focus-visible:after {
:host([design="Emphasized"][desktop]:not([loading])) .ui5-button-root:focus-within:after,
:host([design="Emphasized"]:not([loading])) .ui5-button-root:focus-visible:after {
border-color: var(--_ui5_button_emphasized_focused_border_color);
}

:host([design="Emphasized"][desktop]) .ui5-button-root:focus-within:before,
:host([design="Emphasized"]) .ui5-button-root:focus-visible:before {
:host([design="Emphasized"][desktop]:not([loading])) .ui5-button-root:focus-within:before,
:host([design="Emphasized"]:not([loading])) .ui5-button-root:focus-visible:before {
content: "";
position: absolute;
box-sizing: border-box;
Expand Down Expand Up @@ -173,8 +173,8 @@ bdi {
}

/*The ui5_hovered class is set by FileUploader to indicate hover state of the control*/
:host([design="Positive"]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host([design="Positive"]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) {
:host([design="Positive"]:not([active]):not([loading]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host([design="Positive"]:not([active]):not([loading]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) {
background-color: var(--sapButton_Accept_Hover_Background);
border-color: var(--sapButton_Accept_Hover_BorderColor);
color: var(--sapButton_Accept_Hover_TextColor);
Expand All @@ -193,8 +193,8 @@ bdi {
}

/*The ui5_hovered class is set by FileUploader to indicate hover state of the control*/
:host([design="Negative"]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host([design="Negative"]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) {
:host([design="Negative"]:not([active]):not([loading]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host([design="Negative"]:not([active]):not([loading]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) {
background-color: var(--sapButton_Reject_Hover_Background);
border-color: var(--sapButton_Reject_Hover_BorderColor);
color: var(--sapButton_Reject_Hover_TextColor);
Expand All @@ -213,8 +213,8 @@ bdi {
}

/*The ui5_hovered class is set by FileUploader to indicate hover state of the control*/
:host([design="Attention"]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host([design="Attention"]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) {
:host([design="Attention"]:not([active]):not([loading]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host([design="Attention"]:not([active]):not([loading]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) {
background-color: var(--sapButton_Attention_Hover_Background);
border-color: var(--sapButton_Attention_Hover_BorderColor);
color: var(--sapButton_Attention_Hover_TextColor);
Expand All @@ -235,8 +235,8 @@ bdi {
}

/*The ui5_hovered class is set by FileUploader to indicate hover state of the control*/
:host([design="Emphasized"]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host([design="Emphasized"]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) {
:host([design="Emphasized"]:not([active]):not([loading]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host([design="Emphasized"]:not([active]):not([loading]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) {
background-color: var(--sapButton_Emphasized_Hover_Background);
border-color: var(--sapButton_Emphasized_Hover_BorderColor);
border-width: var(--_ui5_button_emphasized_border_width);
Expand Down Expand Up @@ -268,8 +268,8 @@ bdi {
}

/*The ui5_hovered class is set by FileUploader to indicate hover state of the control*/
:host([design="Transparent"]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host([design="Transparent"]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) {
:host([design="Transparent"]:not([loading]):not([active]):not([non-interactive]):not([_is-touch]):not([disabled]):hover),
:host([design="Transparent"]:not([loading]):not([active]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) {
background-color: var(--sapButton_Lite_Hover_Background);
border-color: var(--sapButton_Lite_Hover_BorderColor);
color: var(--sapButton_Lite_Hover_TextColor);
Expand Down Expand Up @@ -337,4 +337,44 @@ bdi {
:host(:state(has-overlay-badge)) {
overflow: visible;
margin-inline-end: 0.3125rem;
}

:host([loading]){
position: relative;
pointer-events: unset;
border: inherit;
}

:host([loading]) .ui5-button-root {
opacity: var(--sapContent_DisabledOpacity);
border: var(--sapButton_BorderWidth) solid var(--sapButton_BorderColor);
border-radius: var(--_ui5_button_border_radius);
}

:host([loading][design="Emphasized"]) {
background-color: inherit;
border: inherit;
}

:host([loading][design="Emphasized"]) .ui5-button-root {
background-color: var(--sapButton_Emphasized_Background);
border-color: var(--sapButton_Emphasized_BorderColor);
}

:host([loading][design="Positive"]) .ui5-button-root {
border-color: var(--sapButton_Accept_BorderColor);
}

:host([loading][design="Negative"]) .ui5-button-root {
border-color: var(--sapButton_Reject_BorderColor);
}

:host([loading][design="Attention"]) .ui5-button-root {
border-color: var(--sapButton_Attention_BorderColor);
}
.ui5-button-busy-indicator {
position: absolute;
height: 100%;
width: 100%;
top:0;
}
11 changes: 11 additions & 0 deletions packages/main/test/pages/Button.html
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,17 @@
<ui5-button icon="arrow-down" tooltip="Go down"></ui5-button>
<ui5-button icon="arrow-down" disabled tooltip="Go down"></ui5-button>

<br />
<br />
<ui5-title>Buttons with busy indicator</ui5-title>
<br />
<ui5-button tooltip="Button with busy indicator" icon="accept" loading="true"></ui5-button>
<ui5-button tooltip="Button with busy indicator" loading="true">Busy Indicator</ui5-button>
<ui5-button design="Transparent" tooltip="Button with busy indicator" loading="true">Busy Indicator</ui5-button>
<ui5-button tooltip="Button with busy indicator" icon="home" design="Emphasized" loading="true">Busy Indicator</ui5-button>
<ui5-button design="Positive" tooltip="Button with busy indicator" loading="true">Busy Indicator</ui5-button>
<ui5-button design="Negative" tooltip="Button with busy indicator" loading="true">Busy Indicator</ui5-button>
<ui5-button design="Attention" tooltip="Button with busy indicator" loading="true">Busy Indicator</ui5-button>

<br />
<br />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import EndIcon from "../../../_samples/main/Button/EndIcon/EndIcon.md";
import CustomStyling from "../../../_samples/main/Button/CustomStyling/CustomStyling.md";
import MenuButton from "../../../_samples/main/Button/MenuButton/MenuButton.md";
import ButtonBadge from "../../../_samples/main/Button/ButtonBadge/ButtonBadge.md";
import Loading from "../../../_samples/main/Button/Loading/Loading.md"

<%COMPONENT_OVERVIEW%>

Expand Down Expand Up @@ -49,4 +50,7 @@ Achieve Menu Button functionality.
### Button with badge
Add a badge to the button.

<ButtonBadge />
<ButtonBadge />

### Button with Loading
<Loading />
4 changes: 4 additions & 0 deletions packages/website/docs/_samples/main/Button/Loading/Loading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import html from '!!raw-loader!./sample.html';
import js from '!!raw-loader!./main.js';

<Editor html={html} js={js} />
4 changes: 4 additions & 0 deletions packages/website/docs/_samples/main/Button/Loading/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import "@ui5/webcomponents/dist/Button.js";

import "@ui5/webcomponents-icons/dist/edit.js";
import "@ui5/webcomponents-icons/dist/employee.js";
32 changes: 32 additions & 0 deletions packages/website/docs/_samples/main/Button/Loading/sample.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!-- playground-fold -->
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sample</title>
</head>

<body style="background-color: var(--sapBackgroundColor)">
<!-- playground-fold-end -->
<div>
<br/>
<ui5-button icon="sap-icon://edit" tooltip="Accept terms & conditions" loading></ui5-button>
<ui5-button loading>Loading</ui5-button>
<ui5-button icon="sap-icon://employee" loading>Loading</ui5-button>
<ui5-button design="Transparent" loading>Loading</ui5-button>
<ui5-button icon="sap-icon://employee" design="Transparent" loading>Loading</ui5-button>
<ui5-button design="Emphasized" loading>Loading</ui5-button>
<ui5-button icon="sap-icon://employee" design="Emphasized" loading>Loading</ui5-button>
<ui5-button design="Positive" loading>Loading</ui5-button>
<ui5-button icon="sap-icon://employee" design="Positive" loading>Loading</ui5-button>
<ui5-button design="Negative" loading>Loading</ui5-button>
<ui5-button icon="sap-icon://employee" design="Negative" loading>Loading</ui5-button>
</div>
<!-- playground-fold -->
<script type="module" src="main.js"></script>
</body>

</html>
<!-- playground-fold-end -->
Loading