Skip to content

Commit 4b47167

Browse files
committed
fix(material/button): combine MatButton and MatAnchor
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.
1 parent 01292a1 commit 4b47167

File tree

8 files changed

+100
-207
lines changed

8 files changed

+100
-207
lines changed

src/material/button/button-base.ts

Lines changed: 47 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ import {
1313
booleanAttribute,
1414
Directive,
1515
ElementRef,
16+
HostAttributeToken,
1617
inject,
1718
InjectionToken,
1819
Input,
1920
NgZone,
2021
numberAttribute,
2122
OnDestroy,
22-
OnInit,
2323
Renderer2,
2424
} from '@angular/core';
2525
import {_StructuralStylesLoader, MatRippleLoader, ThemePalette} from '@angular/material/core';
@@ -52,6 +52,7 @@ export const MAT_BUTTON_HOST = {
5252
// wants to target all Material buttons.
5353
'[class.mat-mdc-button-base]': 'true',
5454
'[class]': 'color ? "mat-" + color : ""',
55+
'[attr.tabindex]': '_getTabIndex()',
5556
};
5657

5758
/** List of classes to add to buttons instances based on host attribute selector. */
@@ -94,13 +95,17 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
9495
_animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});
9596

9697
private readonly _focusMonitor = inject(FocusMonitor);
98+
private _cleanupClick: (() => void) | undefined;
9799

98100
/**
99101
* Handles the lazy creation of the MatButton ripple.
100102
* Used to improve initial load time of large applications.
101103
*/
102104
protected _rippleLoader: MatRippleLoader = inject(MatRippleLoader);
103105

106+
/** Whether the button is set on an anchor node. */
107+
protected _isAnchor: boolean;
108+
104109
/** Whether this button is a FAB. Used to apply the correct class on the ripple. */
105110
protected _isFab = false;
106111

@@ -153,18 +158,28 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
153158
@Input({transform: booleanAttribute})
154159
disabledInteractive: boolean;
155160

161+
@Input({
162+
transform: (value: unknown) => (value == null ? undefined : numberAttribute(value)),
163+
})
164+
tabIndex: number;
165+
156166
constructor(...args: unknown[]);
157167

158168
constructor() {
159169
inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader);
160170
const config = inject(MAT_BUTTON_CONFIG, {optional: true});
161-
const element = this._elementRef.nativeElement;
171+
const element: HTMLElement = this._elementRef.nativeElement;
162172
const classList = (element as HTMLElement).classList;
163173

174+
this._isAnchor = element.tagName === 'A';
164175
this.disabledInteractive = config?.disabledInteractive ?? false;
165176
this.color = config?.color ?? null;
166177
this._rippleLoader?.configureRipple(element, {className: 'mat-mdc-button-ripple'});
167178

179+
if (this._isAnchor) {
180+
this._setupAsAnchor();
181+
}
182+
168183
// For each of the variant selectors that is present in the button's host
169184
// attributes, add the correct corresponding MDC classes.
170185
for (const {attribute, mdcClasses} of HOST_SELECTOR_MDC_CLASS_PAIR) {
@@ -179,6 +194,7 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
179194
}
180195

181196
ngOnDestroy() {
197+
this._cleanupClick?.();
182198
this._focusMonitor.stopMonitoring(this._elementRef);
183199
this._rippleLoader?.destroyRipple(this._elementRef.nativeElement);
184200
}
@@ -193,10 +209,9 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
193209
}
194210

195211
protected _getAriaDisabled() {
196-
if (this.ariaDisabled != null) {
197-
return this.ariaDisabled;
212+
if (this._isAnchor) {
213+
return this.ariaDisabled != null ? this.ariaDisabled : this.disabled || null;
198214
}
199-
200215
return this.disabled && this.disabledInteractive ? true : null;
201216
}
202217

@@ -210,74 +225,41 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
210225
this.disableRipple || this.disabled,
211226
);
212227
}
213-
}
214-
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"',
223228

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-
};
229+
protected _getTabIndex() {
230+
if (this._isAnchor) {
231+
return this.disabled && !this.disabledInteractive ? -1 : this.tabIndex;
232+
}
233+
return this.tabIndex;
234+
}
238235

239-
/**
240-
* Anchor button base.
241-
*/
242-
@Directive()
243-
export class MatAnchorBase extends MatButtonBase implements OnInit, OnDestroy {
244-
private _renderer = inject(Renderer2);
245-
private _cleanupClick: () => void;
236+
private _setupAsAnchor() {
237+
const renderer = inject(Renderer2);
238+
const initialTabIndex = inject(new HostAttributeToken('tabindex'), {optional: true});
246239

247-
@Input({
248-
transform: (value: unknown) => {
249-
return value == null ? undefined : numberAttribute(value);
250-
},
251-
})
252-
tabIndex: number;
240+
if (initialTabIndex !== null) {
241+
this.tabIndex = numberAttribute(initialTabIndex, undefined);
242+
}
253243

254-
ngOnInit(): void {
255244
this._ngZone.runOutsideAngular(() => {
256-
this._cleanupClick = this._renderer.listen(
245+
this._cleanupClick = renderer.listen(
257246
this._elementRef.nativeElement,
258247
'click',
259-
this._haltDisabledEvents,
248+
(event: Event) => {
249+
if (this.disabled) {
250+
event.preventDefault();
251+
event.stopImmediatePropagation();
252+
}
253+
},
260254
);
261255
});
262256
}
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-
}
283257
}
258+
259+
// tslint:disable:variable-name
260+
/**
261+
* Anchor button base.
262+
*/
263+
export const MatAnchorBase = MatButtonBase;
264+
export type MatAnchorBase = MatButtonBase;
265+
// 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)