diff --git a/packages/main/cypress/specs/Button.cy.tsx b/packages/main/cypress/specs/Button.cy.tsx index 17b46a6994eb..53fa03ba40a1 100644 --- a/packages/main/cypress/specs/Button.cy.tsx +++ b/packages/main/cypress/specs/Button.cy.tsx @@ -205,6 +205,29 @@ describe("Button general interaction", () => { .should("not.called"); }); + it("tests clicking on button with busy indicator", () => { + cy.mount(); + + 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(); @@ -453,6 +476,18 @@ describe("Accessibility", () => { .should("have.attr", "aria-controls", "registration-dialog"); }); + it("aria-busy is properly applied on the button with busy indicator", () => { + cy.mount(); + + cy.get("[ui5-button]") + .as("button"); + + cy.get("@button") + .shadow() + .find("button") + .should("have.attr", "aria-busy", "true"); + }); + it("setting accessible-description is applied to button tag", () => { cy.mount(); diff --git a/packages/main/src/Button.ts b/packages/main/src/Button.ts index ef9f6f123057..5b26bf80232e 100644 --- a/packages/main/src/Button.ts +++ b/packages/main/src/Button.ts @@ -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 */ @property({ noAttribute: true }) @@ -448,6 +469,11 @@ class Button extends UI5Element implements IButton { return; } + if (this.loading) { + e.preventDefault(); + return; + } + const { altKey, ctrlKey, @@ -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(); } @@ -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; } diff --git a/packages/main/src/ButtonTemplate.tsx b/packages/main/src/ButtonTemplate.tsx index 2c101fcadd04..8fe9cd834028 100644 --- a/packages/main/src/ButtonTemplate.tsx +++ b/packages/main/src/ButtonTemplate.tsx @@ -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, @@ -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 @@ -37,6 +38,7 @@ export default function ButtonTemplate(this: Button, injectedProps?: { aria-haspopup={this._hasPopup} aria-label={this.ariaLabelText} aria-description={this.ariaDescriptionText} + aria-busy={this.loading ? "true" : undefined} title={this.buttonTitle} part="button" role={this.effectiveAccRole} @@ -69,5 +71,15 @@ export default function ButtonTemplate(this: Button, injectedProps?: { } + {this.loading && + + } ); } diff --git a/packages/main/src/themes/Button.css b/packages/main/src/themes/Button.css index bd59cddc4c3f..511decc726b3 100644 --- a/packages/main/src/themes/Button.css +++ b/packages/main/src/themes/Button.css @@ -108,7 +108,7 @@ pointer-events: none; } -:host([desktop]:not([active])) .ui5-button-root:focus-within:after, +:host([desktop]:not([loading])) .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, @@ -337,4 +337,41 @@ bdi { :host(:state(has-overlay-badge)) { overflow: visible; margin-inline-end: 0.3125rem; -} \ No newline at end of file +} + +:host([loading]){ + position: relative; + pointer-events: unset; +} + +:host([loading]) .ui5-button-root { + opacity: var(--sapContent_DisabledOpacity); +} + +:host([loading][design="Emphasized"]) { + background-color: inherit; + border: inherit; +} + +:host([design="Emphasized"][loading]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]):hover), +:host([design="Emphasized"][loading]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) { + background-color: inherit; + border: inherit; +} + +:host([design="Emphasized"][loading]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]):hover) .ui5-button-root, +:host([design="Emphasized"][loading]:not([active]):not([non-interactive]):not([_is-touch]):not([disabled]).ui5_hovered) .ui5-button-root { + background-color: var(--sapButton_Emphasized_Hover_Background); +} + +:host([loading][design="Emphasized"]) .ui5-button-root { + background-color: var(--sapButton_Emphasized_Background); + border-color: var(--sapButton_Emphasized_BorderColor); +} + +.ui5-button-busy-indicator { + position: absolute; + height: 100%; + width: 100%; + top:0; +} diff --git a/packages/main/test/pages/Button.html b/packages/main/test/pages/Button.html index 1da137f840e1..e33b12c4c453 100644 --- a/packages/main/test/pages/Button.html +++ b/packages/main/test/pages/Button.html @@ -265,6 +265,17 @@ +
+
+ Buttons with busy indicator +
+ + Busy Indicator + Busy Indicator + Busy Indicator + Busy Indicator + Busy Indicator + Busy Indicator

diff --git a/packages/website/docs/_components_pages/main/Button/Button.mdx b/packages/website/docs/_components_pages/main/Button/Button.mdx index d3ebf9ef4350..a98e89f21c47 100644 --- a/packages/website/docs/_components_pages/main/Button/Button.mdx +++ b/packages/website/docs/_components_pages/main/Button/Button.mdx @@ -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%> @@ -49,4 +50,7 @@ Achieve Menu Button functionality. ### Button with badge Add a badge to the button. - \ No newline at end of file + + +### Button with Loading + \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Button/Loading/Loading.md b/packages/website/docs/_samples/main/Button/Loading/Loading.md new file mode 100644 index 000000000000..66ccdc236d97 --- /dev/null +++ b/packages/website/docs/_samples/main/Button/Loading/Loading.md @@ -0,0 +1,4 @@ +import html from '!!raw-loader!./sample.html'; +import js from '!!raw-loader!./main.js'; + + \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Button/Loading/main.js b/packages/website/docs/_samples/main/Button/Loading/main.js new file mode 100644 index 000000000000..1e5482b9eca0 --- /dev/null +++ b/packages/website/docs/_samples/main/Button/Loading/main.js @@ -0,0 +1,4 @@ +import "@ui5/webcomponents/dist/Button.js"; + +import "@ui5/webcomponents-icons/dist/edit.js"; +import "@ui5/webcomponents-icons/dist/employee.js"; \ No newline at end of file diff --git a/packages/website/docs/_samples/main/Button/Loading/sample.html b/packages/website/docs/_samples/main/Button/Loading/sample.html new file mode 100644 index 000000000000..a276e988ec23 --- /dev/null +++ b/packages/website/docs/_samples/main/Button/Loading/sample.html @@ -0,0 +1,32 @@ + + + + + + + + Sample + + + + +
+
+ + Loading + Loading + Loading + Loading + Loading + Loading + Loading + Loading + Loading + Loading +
+ + + + + + \ No newline at end of file