From 3352a2cbaa3435a067282425b7eb5545f567394b Mon Sep 17 00:00:00 2001 From: Jeremy Mowery Date: Sun, 25 Jan 2026 19:06:02 -0800 Subject: [PATCH] feat(material/button): Add support for showing a progress indicator inside the button Add a new API, testing API, and docs for allowing progress indicators to be projected into a button and have them shown in an accessible manner Fixes #13667 --- goldens/material/button/index.api.md | 8 +- goldens/material/button/testing/index.api.md | 1 + .../material/button/BUILD.bazel | 2 + .../button-overview-example.html | 91 ++++++ .../button-overview-example.ts | 3 +- .../button-progress-indicator-example.html | 29 ++ .../button-progress-indicator-example.ts | 20 ++ .../material/button/index.ts | 1 + src/dev-app/button/BUILD.bazel | 1 + src/dev-app/button/button-demo.html | 262 +++++++++++++++--- src/dev-app/button/button-demo.ts | 19 +- src/material/button/BUILD.bazel | 1 + src/material/button/_m3-button.scss | 1 + src/material/button/button-base.ts | 5 + src/material/button/button.html | 21 +- src/material/button/button.md | 8 + src/material/button/button.scss | 28 ++ src/material/button/button.spec.ts | 91 +++++- src/material/button/fab.scss | 19 ++ src/material/button/icon-button.html | 6 + src/material/button/icon-button.scss | 19 +- .../button/testing/button-harness.spec.ts | 52 +++- src/material/button/testing/button-harness.ts | 7 + 23 files changed, 632 insertions(+), 63 deletions(-) create mode 100644 src/components-examples/material/button/button-progress-indicator/button-progress-indicator-example.html create mode 100644 src/components-examples/material/button/button-progress-indicator/button-progress-indicator-example.ts diff --git a/goldens/material/button/index.api.md b/goldens/material/button/index.api.md index e5854c39659e..f8956808ac19 100644 --- a/goldens/material/button/index.api.md +++ b/goldens/material/button/index.api.md @@ -33,7 +33,7 @@ export class MatButton extends MatButtonBase { set appearance(value: MatButtonAppearance | ''); setAppearance(appearance: MatButtonAppearance): void; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -74,7 +74,7 @@ export class MatFabButton extends MatButtonBase { // (undocumented) static ngAcceptInputType_extended: unknown; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -94,7 +94,7 @@ export type MatIconAnchor = MatIconButton; export class MatIconButton extends MatButtonBase { constructor(...args: unknown[]); // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -111,7 +111,7 @@ export class MatMiniFabButton extends MatButtonBase { // (undocumented) _isFab: boolean; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } diff --git a/goldens/material/button/testing/index.api.md b/goldens/material/button/testing/index.api.md index c57ebcb1c0d9..3d82c0b2fa40 100644 --- a/goldens/material/button/testing/index.api.md +++ b/goldens/material/button/testing/index.api.md @@ -42,6 +42,7 @@ export class MatButtonHarness extends ContentContainerComponentHarness { static hostSelector: string; isDisabled(): Promise; isFocused(): Promise; + isShowingProgress(): Promise; static with(this: ComponentHarnessConstructor, options?: ButtonHarnessFilters): HarnessPredicate; } diff --git a/src/components-examples/material/button/BUILD.bazel b/src/components-examples/material/button/BUILD.bazel index 04468715abbc..29180a79587e 100644 --- a/src/components-examples/material/button/BUILD.bazel +++ b/src/components-examples/material/button/BUILD.bazel @@ -20,8 +20,10 @@ ng_project( "//src/cdk/testing/testbed", "//src/material/button", "//src/material/button/testing", + "//src/material/checkbox", "//src/material/divider", "//src/material/icon", + "//src/material/progress-spinner", "//src/material/tooltip", ], ) diff --git a/src/components-examples/material/button/button-overview/button-overview-example.html b/src/components-examples/material/button/button-overview/button-overview-example.html index f2dee64faf0f..063a7c5d9ddc 100644 --- a/src/components-examples/material/button/button-overview/button-overview-example.html +++ b/src/components-examples/material/button/button-overview/button-overview-example.html @@ -4,6 +4,16 @@ Link + @@ -13,6 +23,16 @@ Link + @@ -22,6 +42,16 @@ Link + @@ -31,6 +61,16 @@ Link + @@ -40,6 +80,16 @@ Link + @@ -53,6 +103,16 @@ + @@ -67,6 +127,16 @@ + @@ -81,6 +151,16 @@ + @@ -101,6 +181,17 @@ favorite Link + diff --git a/src/components-examples/material/button/button-overview/button-overview-example.ts b/src/components-examples/material/button/button-overview/button-overview-example.ts index c7783cb7673e..759ad348f153 100644 --- a/src/components-examples/material/button/button-overview/button-overview-example.ts +++ b/src/components-examples/material/button/button-overview/button-overview-example.ts @@ -2,6 +2,7 @@ import {Component} from '@angular/core'; import {MatIconModule} from '@angular/material/icon'; import {MatDividerModule} from '@angular/material/divider'; import {MatButtonModule} from '@angular/material/button'; +import {MatProgressSpinner} from '@angular/material/progress-spinner'; /** * @title Button overview @@ -10,6 +11,6 @@ import {MatButtonModule} from '@angular/material/button'; selector: 'button-overview-example', templateUrl: 'button-overview-example.html', styleUrl: 'button-overview-example.css', - imports: [MatButtonModule, MatDividerModule, MatIconModule], + imports: [MatButtonModule, MatDividerModule, MatIconModule, MatProgressSpinner], }) export class ButtonOverviewExample {} diff --git a/src/components-examples/material/button/button-progress-indicator/button-progress-indicator-example.html b/src/components-examples/material/button/button-progress-indicator/button-progress-indicator-example.html new file mode 100644 index 000000000000..204e9e44e4fb --- /dev/null +++ b/src/components-examples/material/button/button-progress-indicator/button-progress-indicator-example.html @@ -0,0 +1,29 @@ +
+ + Show the progress indicator + +
+
+

Button with a MatProgressSpinner

+ +
+
+

Button with a custom progress indicator

+ +
\ No newline at end of file diff --git a/src/components-examples/material/button/button-progress-indicator/button-progress-indicator-example.ts b/src/components-examples/material/button/button-progress-indicator/button-progress-indicator-example.ts new file mode 100644 index 000000000000..b0200632b05f --- /dev/null +++ b/src/components-examples/material/button/button-progress-indicator/button-progress-indicator-example.ts @@ -0,0 +1,20 @@ +import {Component, signal} from '@angular/core'; +import {MatButton} from '@angular/material/button'; +import {MatCheckbox} from '@angular/material/checkbox'; +import {MatProgressSpinner} from '@angular/material/progress-spinner'; + +/** + * @title Buttons with progress indicators + */ +@Component({ + selector: 'button-progress-indicator-example', + templateUrl: 'button-progress-indicator-example.html', + imports: [MatButton, MatCheckbox, MatProgressSpinner], +}) +export class ButtonProgressIndicatorExample { + protected readonly showProgress = signal(false); + + protected toggleShowProgress() { + this.showProgress.update(show => !show); + } +} diff --git a/src/components-examples/material/button/index.ts b/src/components-examples/material/button/index.ts index 4557e8edb62e..e60749acc079 100644 --- a/src/components-examples/material/button/index.ts +++ b/src/components-examples/material/button/index.ts @@ -1,3 +1,4 @@ export {ButtonOverviewExample} from './button-overview/button-overview-example'; export {ButtonDisabledInteractiveExample} from './button-disabled-interactive/button-disabled-interactive-example'; export {ButtonHarnessExample} from './button-harness/button-harness-example'; +export {ButtonProgressIndicatorExample} from './button-progress-indicator/button-progress-indicator-example'; diff --git a/src/dev-app/button/BUILD.bazel b/src/dev-app/button/BUILD.bazel index c94c68c9b705..fe4c5d24315a 100644 --- a/src/dev-app/button/BUILD.bazel +++ b/src/dev-app/button/BUILD.bazel @@ -15,6 +15,7 @@ ng_project( "//src/material/button", "//src/material/checkbox", "//src/material/icon", + "//src/material/progress-spinner", "//src/material/tooltip", ], ) diff --git a/src/dev-app/button/button-demo.html b/src/dev-app/button/button-demo.html index 2d96f2740820..7e70494a204c 100644 --- a/src/dev-app/button/button-demo.html +++ b/src/dev-app/button/button-demo.html @@ -23,20 +23,20 @@

Buttons

[matButton]="appearance" disabled [disabledInteractive]="disabledInteractive" - [matTooltip]="tooltipText">{{appearance}} + [matTooltip]="tooltipText" + > + {{appearance}} + } - + [matTooltip]="tooltipText" + > + Search + + +
+ @for (appearance of appearances; track $index) { + + } + + + +
@for (appearance of appearances; track $index) { -

{{appearance}} Appearance

+

+ {{appearance}} Appearance +

@@ -149,7 +225,10 @@

{{appearance}} Appearance

[matButton]="appearance" disabled [disabledInteractive]="disabledInteractive" - [matTooltip]="tooltipText">disabled + [matTooltip]="tooltipText" + > + disabled + +
} @@ -180,9 +270,20 @@

Icon Buttons

matIconButton disabled [disabledInteractive]="disabledInteractive" - [matTooltip]="tooltipText"> + [matTooltip]="tooltipText" + > visibility +

Icon Button Anchors

@@ -204,7 +305,8 @@

Icon Button Anchors

matIconButton disabled [disabledInteractive]="disabledInteractive" - [matTooltip]="tooltipText"> + [matTooltip]="tooltipText" + > visibility
@@ -223,13 +325,19 @@

FABs

- +

Mini FABs

@@ -246,11 +354,22 @@

Mini FABs

+ @@ -261,37 +380,106 @@

Interaction/State

isDisabled: {{isDisabled}}

Button 1 as been clicked {{clickCounter}} times

- Allow disabled button interactivity + Allow disabled button interactivity

All disabled

+

+ All showProgress +

- - - + Button 3 + - - -
diff --git a/src/dev-app/button/button-demo.ts b/src/dev-app/button/button-demo.ts index b8b8fa4b42f3..79725c59bbb7 100644 --- a/src/dev-app/button/button-demo.ts +++ b/src/dev-app/button/button-demo.ts @@ -21,6 +21,7 @@ import { } from '@angular/material/button'; import {MatCheckbox} from '@angular/material/checkbox'; import {MatIcon} from '@angular/material/icon'; +import {MatProgressSpinner} from '@angular/material/progress-spinner'; import {MatTooltip} from '@angular/material/tooltip'; @Component({ @@ -28,23 +29,25 @@ import {MatTooltip} from '@angular/material/tooltip'; templateUrl: 'button-demo.html', styleUrl: 'button-demo.css', imports: [ - MatButton, + FormsModule, MatAnchor, - MatFabButton, + MatButton, + MatCheckbox, MatFabAnchor, - MatMiniFabButton, - MatMiniFabAnchor, - MatIconButton, - MatIconAnchor, + MatFabButton, MatIcon, + MatIconAnchor, + MatIconButton, + MatMiniFabAnchor, + MatMiniFabButton, + MatProgressSpinner, MatTooltip, - MatCheckbox, - FormsModule, ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ButtonDemo { isDisabled = false; + showProgress = false; clickCounter = 0; toggleDisable = false; tooltipText = 'This is a button tooltip!'; diff --git a/src/material/button/BUILD.bazel b/src/material/button/BUILD.bazel index 9e35c2644dd7..9671cabd541c 100644 --- a/src/material/button/BUILD.bazel +++ b/src/material/button/BUILD.bazel @@ -135,6 +135,7 @@ sass_binary( "//src/material/core/style:private", "//src/material/core/style:vendor_prefixes", "//src/material/core/tokens:token_utils", + "//src/material/progress-spinner:theme", ], ) diff --git a/src/material/button/_m3-button.scss b/src/material/button/_m3-button.scss index 4d52673eb524..a388b17234cc 100644 --- a/src/material/button/_m3-button.scss +++ b/src/material/button/_m3-button.scss @@ -63,6 +63,7 @@ button-filled-pressed-state-layer-opacity:map.get($system, pressed-state-layer-opacity), button-filled-ripple-color: m3-utils.color-with-opacity( map.get($system, on-primary), map.get($system, pressed-state-layer-opacity)), + button-filled-progress-active-indicator-color: map.get($system, on-primary), button-filled-state-layer-color: map.get($system, on-primary), button-outlined-disabled-label-text-color: m3-utils.color-with-opacity(map.get($system, on-surface), 38%), diff --git a/src/material/button/button-base.ts b/src/material/button/button-base.ts index c4e6957ef140..2c48ba787740 100644 --- a/src/material/button/button-base.ts +++ b/src/material/button/button-base.ts @@ -14,6 +14,7 @@ import { ElementRef, inject, InjectionToken, + input, Input, NgZone, numberAttribute, @@ -55,6 +56,7 @@ function transformTabIndex(value: unknown): number | undefined { // wants to target all Material buttons. 'class': 'mat-mdc-button-base', '[class]': 'color ? "mat-" + color : ""', + '[class.mat-mdc-button-progress-indicator-shown]': 'showProgress()', '[attr.disabled]': '_getDisabledAttribute()', '[attr.aria-disabled]': '_getAriaDisabled()', '[attr.tabindex]': '_getTabIndex()', @@ -150,6 +152,9 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { this.tabIndex = value; } + /** Whether the button is showing a progress indicator. */ + readonly showProgress = input(false, {transform: booleanAttribute}); + constructor(...args: unknown[]); constructor() { diff --git a/src/material/button/button.html b/src/material/button/button.html index 0ddd2794ef91..b03f650aeb8f 100644 --- a/src/material/button/button.html +++ b/src/material/button/button.html @@ -1,16 +1,27 @@ + class="mat-mdc-button-persistent-ripple" + [class.mdc-button__ripple]="!_isFab" + [class.mdc-fab__ripple]="_isFab" +> - + - + +@if (showProgress()) { +
+ +
+} + +### Buttons with progress indicators +An element with the `progressIndicator` attribute may be projected into the button element. When the `showProgress` input is `true` this element will be shown over the content of the button and the content of the button will be made invisible. + + + ### Accessibility Angular Material uses native ` + [color]="buttonColor" [disabledInteractive]="disabledInteractive" + [showProgress]="showProgress"> Link + Progress... - + - + `, @@ -469,6 +551,7 @@ class TestApp { extended = false; disabledInteractive = false; appearance: MatButtonAppearance = 'text'; + showProgress = false; increment() { this.clickCount++; diff --git a/src/material/button/fab.scss b/src/material/button/fab.scss index de6e03f23c1e..42071234f8e0 100644 --- a/src/material/button/fab.scss +++ b/src/material/button/fab.scss @@ -229,3 +229,22 @@ $fallbacks: m3-fab.get-tokens(); } } +.mat-mdc-button-progress-indicator-container { + position: absolute; + inset-inline-start: 0; + margin-block-start: 0; + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + box-sizing: border-box; +} + +.mat-mdc-button-progress-indicator-shown { + mat-icon, + [matButtonIcon], + .mdc-button__label { + visibility: hidden; + } +} diff --git a/src/material/button/icon-button.html b/src/material/button/icon-button.html index c40c3f3ab137..762a605197a0 100644 --- a/src/material/button/icon-button.html +++ b/src/material/button/icon-button.html @@ -2,6 +2,12 @@ +@if (showProgress()) { +
+ +
+} +