Skip to content

Commit 6a5943d

Browse files
authored
fix(material/button): combine MatButton and MatAnchor (#30492)
Currently we have two directives for each button variant: `MatButton` which applies to `button` elements and `MatButtonAnchor` which applies to anchors. This is problematic in a couple of ways: 1. The styles, which can be non-trivial, are duplicated if both classes are used. 2. Users have to think about which class they're importing. These changes combine the two classes to resolve the issues and simplify our setup. BREAKING CHANGE: `tabindex` values set as `[attr.tabindex]` set on a Material button might not work as expected. Use `tabindex` for static values, or `[tabindex]`/`[tabIndex]` for dynamic ones.
1 parent 41d6353 commit 6a5943d

File tree

8 files changed

+110
-209
lines changed

8 files changed

+110
-209
lines changed

src/material/button/button-base.ts

Lines changed: 57 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
NgZone,
2020
numberAttribute,
2121
OnDestroy,
22-
OnInit,
2322
Renderer2,
2423
} from '@angular/core';
2524
import {_StructuralStylesLoader, MatRippleLoader, ThemePalette} from '@angular/material/core';
@@ -52,8 +51,13 @@ export const MAT_BUTTON_HOST = {
5251
// wants to target all Material buttons.
5352
'[class.mat-mdc-button-base]': 'true',
5453
'[class]': 'color ? "mat-" + color : ""',
54+
'[attr.tabindex]': '_getTabIndex()',
5555
};
5656

57+
function transformTabIndex(value: unknown): number | undefined {
58+
return value == null ? undefined : numberAttribute(value);
59+
}
60+
5761
/** List of classes to add to buttons instances based on host attribute selector. */
5862
const HOST_SELECTOR_MDC_CLASS_PAIR: {attribute: string; mdcClasses: string[]}[] = [
5963
{
@@ -94,13 +98,18 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
9498
_animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});
9599

96100
private readonly _focusMonitor = inject(FocusMonitor);
101+
private _cleanupClick: (() => void) | undefined;
102+
private _renderer = inject(Renderer2);
97103

98104
/**
99105
* Handles the lazy creation of the MatButton ripple.
100106
* Used to improve initial load time of large applications.
101107
*/
102108
protected _rippleLoader: MatRippleLoader = inject(MatRippleLoader);
103109

110+
/** Whether the button is set on an anchor node. */
111+
protected _isAnchor: boolean;
112+
104113
/** Whether this button is a FAB. Used to apply the correct class on the ripple. */
105114
protected _isFab = false;
106115

@@ -153,14 +162,28 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
153162
@Input({transform: booleanAttribute})
154163
disabledInteractive: boolean;
155164

165+
/** Tab index for the button. */
166+
@Input({transform: transformTabIndex})
167+
tabIndex: number;
168+
169+
/**
170+
* Backwards-compatibility input that handles pre-existing `[tabindex]` bindings.
171+
* @docs-private
172+
*/
173+
@Input({alias: 'tabindex', transform: transformTabIndex})
174+
set _tabindex(value: number) {
175+
this.tabIndex = value;
176+
}
177+
156178
constructor(...args: unknown[]);
157179

158180
constructor() {
159181
inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader);
160182
const config = inject(MAT_BUTTON_CONFIG, {optional: true});
161-
const element = this._elementRef.nativeElement;
183+
const element: HTMLElement = this._elementRef.nativeElement;
162184
const classList = (element as HTMLElement).classList;
163185

186+
this._isAnchor = element.tagName === 'A';
164187
this.disabledInteractive = config?.disabledInteractive ?? false;
165188
this.color = config?.color ?? null;
166189
this._rippleLoader?.configureRipple(element, {className: 'mat-mdc-button-ripple'});
@@ -176,9 +199,16 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
176199

177200
ngAfterViewInit() {
178201
this._focusMonitor.monitor(this._elementRef, true);
202+
203+
// Some internal tests depend on the timing of this,
204+
// otherwise we could bind it in the constructor.
205+
if (this._isAnchor) {
206+
this._setupAsAnchor();
207+
}
179208
}
180209

181210
ngOnDestroy() {
211+
this._cleanupClick?.();
182212
this._focusMonitor.stopMonitoring(this._elementRef);
183213
this._rippleLoader?.destroyRipple(this._elementRef.nativeElement);
184214
}
@@ -197,6 +227,10 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
197227
return this.ariaDisabled;
198228
}
199229

230+
if (this._isAnchor) {
231+
return this.disabled || null;
232+
}
233+
200234
return this.disabled && this.disabledInteractive ? true : null;
201235
}
202236

@@ -210,74 +244,30 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
210244
this.disableRipple || this.disabled,
211245
);
212246
}
213-
}
214247

215-
/** Shared host configuration for buttons using the `<a>` tag. */
216-
export const MAT_ANCHOR_HOST = {
217-
// Note that this is basically a noop on anchors,
218-
// but it appears that some internal apps depend on it.
219-
'[attr.disabled]': '_getDisabledAttribute()',
220-
'[class.mat-mdc-button-disabled]': 'disabled',
221-
'[class.mat-mdc-button-disabled-interactive]': 'disabledInteractive',
222-
'[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"',
248+
protected _getTabIndex() {
249+
if (this._isAnchor) {
250+
return this.disabled && !this.disabledInteractive ? -1 : this.tabIndex;
251+
}
252+
return this.tabIndex;
253+
}
223254

224-
// Note that we ignore the user-specified tabindex when it's disabled for
225-
// consistency with the `mat-button` applied on native buttons where even
226-
// though they have an index, they're not tabbable.
227-
'[attr.tabindex]': 'disabled && !disabledInteractive ? -1 : tabIndex',
228-
'[attr.aria-disabled]': '_getAriaDisabled()',
229-
// MDC automatically applies the primary theme color to the button, but we want to support
230-
// an unthemed version. If color is undefined, apply a CSS class that makes it easy to
231-
// select and style this "theme".
232-
'[class.mat-unthemed]': '!color',
233-
// Add a class that applies to all buttons. This makes it easier to target if somebody
234-
// wants to target all Material buttons.
235-
'[class.mat-mdc-button-base]': 'true',
236-
'[class]': 'color ? "mat-" + color : ""',
237-
};
255+
private _setupAsAnchor() {
256+
this._cleanupClick = this._ngZone.runOutsideAngular(() =>
257+
this._renderer.listen(this._elementRef.nativeElement, 'click', (event: Event) => {
258+
if (this.disabled) {
259+
event.preventDefault();
260+
event.stopImmediatePropagation();
261+
}
262+
}),
263+
);
264+
}
265+
}
238266

267+
// tslint:disable:variable-name
239268
/**
240269
* Anchor button base.
241270
*/
242-
@Directive()
243-
export class MatAnchorBase extends MatButtonBase implements OnInit, OnDestroy {
244-
private _renderer = inject(Renderer2);
245-
private _cleanupClick: () => void;
246-
247-
@Input({
248-
transform: (value: unknown) => {
249-
return value == null ? undefined : numberAttribute(value);
250-
},
251-
})
252-
tabIndex: number;
253-
254-
ngOnInit(): void {
255-
this._ngZone.runOutsideAngular(() => {
256-
this._cleanupClick = this._renderer.listen(
257-
this._elementRef.nativeElement,
258-
'click',
259-
this._haltDisabledEvents,
260-
);
261-
});
262-
}
263-
264-
override ngOnDestroy(): void {
265-
super.ngOnDestroy();
266-
this._cleanupClick?.();
267-
}
268-
269-
_haltDisabledEvents = (event: Event): void => {
270-
// A disabled button shouldn't apply any actions
271-
if (this.disabled) {
272-
event.preventDefault();
273-
event.stopImmediatePropagation();
274-
}
275-
};
276-
277-
protected override _getAriaDisabled() {
278-
if (this.ariaDisabled != null) {
279-
return this.ariaDisabled;
280-
}
281-
return this.disabled || null;
282-
}
283-
}
271+
export const MatAnchorBase = MatButtonBase;
272+
export type MatAnchorBase = MatButtonBase;
273+
// tslint:enable:variable-name

src/material/button/button.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
10-
import {MAT_ANCHOR_HOST, MAT_BUTTON_HOST, MatAnchorBase, MatButtonBase} from './button-base';
10+
import {MAT_BUTTON_HOST, MatButtonBase} from './button-base';
1111

1212
/**
1313
* Material Design button component. Users interact with a button to perform an action.
@@ -21,17 +21,19 @@ import {MAT_ANCHOR_HOST, MAT_BUTTON_HOST, MatAnchorBase, MatButtonBase} from './
2121
@Component({
2222
selector: `
2323
button[mat-button], button[mat-raised-button], button[mat-flat-button],
24-
button[mat-stroked-button]
24+
button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button],
25+
a[mat-stroked-button]
2526
`,
2627
templateUrl: 'button.html',
2728
styleUrls: ['button.css', 'button-high-contrast.css'],
2829
host: MAT_BUTTON_HOST,
29-
exportAs: 'matButton',
30+
exportAs: 'matButton, matAnchor',
3031
encapsulation: ViewEncapsulation.None,
3132
changeDetection: ChangeDetectionStrategy.OnPush,
3233
})
3334
export class MatButton extends MatButtonBase {}
3435

36+
// tslint:disable:variable-name
3537
/**
3638
* Material Design button component for anchor elements. Anchor elements are used to provide
3739
* links for the user to navigate across different routes or pages.
@@ -42,13 +44,6 @@ export class MatButton extends MatButtonBase {}
4244
* specification. `MatAnchor` additionally captures an additional "flat" appearance, which matches
4345
* "contained" but without elevation.
4446
*/
45-
@Component({
46-
selector: `a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button]`,
47-
exportAs: 'matButton, matAnchor',
48-
host: MAT_ANCHOR_HOST,
49-
templateUrl: 'button.html',
50-
styleUrls: ['button.css', 'button-high-contrast.css'],
51-
encapsulation: ViewEncapsulation.None,
52-
changeDetection: ChangeDetectionStrategy.OnPush,
53-
})
54-
export class MatAnchor extends MatAnchorBase {}
47+
export const MatAnchor = MatButton;
48+
export type MatAnchor = MatButton;
49+
// tslint:enable:variable-name

src/material/button/fab.ts

Lines changed: 11 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import {
1616
inject,
1717
} from '@angular/core';
1818

19-
import {MatAnchor} from './button';
20-
import {MAT_ANCHOR_HOST, MAT_BUTTON_HOST, MatButtonBase} from './button-base';
19+
import {MAT_BUTTON_HOST, MatButtonBase} from './button-base';
2120
import {ThemePalette} from '@angular/material/core';
2221

2322
/** Default FAB options that can be overridden. */
@@ -60,15 +59,15 @@ const defaults = MAT_FAB_DEFAULT_OPTIONS_FACTORY();
6059
* The `MatFabButton` class has two appearances: normal and extended.
6160
*/
6261
@Component({
63-
selector: `button[mat-fab]`,
62+
selector: `button[mat-fab], a[mat-fab]`,
6463
templateUrl: 'button.html',
6564
styleUrl: 'fab.css',
6665
host: {
6766
...MAT_BUTTON_HOST,
6867
'[class.mdc-fab--extended]': 'extended',
6968
'[class.mat-mdc-extended-fab]': 'extended',
7069
},
71-
exportAs: 'matButton',
70+
exportAs: 'matButton, matAnchor',
7271
encapsulation: ViewEncapsulation.None,
7372
changeDetection: ChangeDetectionStrategy.OnPush,
7473
})
@@ -94,11 +93,11 @@ export class MatFabButton extends MatButtonBase {
9493
* See https://material.io/components/buttons-floating-action-button/
9594
*/
9695
@Component({
97-
selector: `button[mat-mini-fab]`,
96+
selector: `button[mat-mini-fab], a[mat-mini-fab]`,
9897
templateUrl: 'button.html',
9998
styleUrl: 'fab.css',
10099
host: MAT_BUTTON_HOST,
101-
exportAs: 'matButton',
100+
exportAs: 'matButton, matAnchor',
102101
encapsulation: ViewEncapsulation.None,
103102
changeDetection: ChangeDetectionStrategy.OnPush,
104103
})
@@ -116,66 +115,22 @@ export class MatMiniFabButton extends MatButtonBase {
116115
}
117116
}
118117

118+
// tslint:disable:variable-name
119119
/**
120120
* Material Design floating action button (FAB) component for anchor elements. Anchor elements
121121
* are used to provide links for the user to navigate across different routes or pages.
122122
* See https://material.io/components/buttons-floating-action-button/
123123
*
124124
* The `MatFabAnchor` class has two appearances: normal and extended.
125125
*/
126-
@Component({
127-
selector: `a[mat-fab]`,
128-
templateUrl: 'button.html',
129-
styleUrl: 'fab.css',
130-
host: {
131-
...MAT_ANCHOR_HOST,
132-
'[class.mdc-fab--extended]': 'extended',
133-
'[class.mat-mdc-extended-fab]': 'extended',
134-
},
135-
exportAs: 'matButton, matAnchor',
136-
encapsulation: ViewEncapsulation.None,
137-
changeDetection: ChangeDetectionStrategy.OnPush,
138-
})
139-
export class MatFabAnchor extends MatAnchor {
140-
private _options = inject<MatFabDefaultOptions>(MAT_FAB_DEFAULT_OPTIONS, {optional: true});
141-
142-
override _isFab = true;
143-
144-
@Input({transform: booleanAttribute}) extended: boolean;
145-
146-
constructor(...args: unknown[]);
147-
148-
constructor() {
149-
super();
150-
this._options = this._options || defaults;
151-
this.color = this._options!.color || defaults.color;
152-
}
153-
}
126+
export const MatFabAnchor = MatFabButton;
127+
export type MatFabAnchor = MatFabButton;
154128

155129
/**
156130
* Material Design mini floating action button (FAB) component for anchor elements. Anchor elements
157131
* are used to provide links for the user to navigate across different routes or pages.
158132
* See https://material.io/components/buttons-floating-action-button/
159133
*/
160-
@Component({
161-
selector: `a[mat-mini-fab]`,
162-
templateUrl: 'button.html',
163-
styleUrl: 'fab.css',
164-
host: MAT_ANCHOR_HOST,
165-
exportAs: 'matButton, matAnchor',
166-
encapsulation: ViewEncapsulation.None,
167-
changeDetection: ChangeDetectionStrategy.OnPush,
168-
})
169-
export class MatMiniFabAnchor extends MatAnchor {
170-
private _options = inject<MatFabDefaultOptions>(MAT_FAB_DEFAULT_OPTIONS, {optional: true});
171-
172-
override _isFab = true;
173-
174-
constructor(...args: unknown[]);
175-
176-
constructor() {
177-
super();
178-
this._options = this._options || defaults;
179-
this.color = this._options!.color || defaults.color;
180-
}
181-
}
134+
export const MatMiniFabAnchor = MatMiniFabButton;
135+
export type MatMiniFabAnchor = MatMiniFabButton;
136+
// tslint:enable:variable-name

0 commit comments

Comments
 (0)