Skip to content

Commit e1f2211

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 e1f2211

File tree

8 files changed

+114
-204
lines changed

8 files changed

+114
-204
lines changed

src/material/button/button-base.ts

Lines changed: 61 additions & 62 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,8 +52,13 @@ 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

58+
function transformTabIndex(value: unknown): number | undefined {
59+
return value == null ? undefined : numberAttribute(value);
60+
}
61+
5762
/** List of classes to add to buttons instances based on host attribute selector. */
5863
const HOST_SELECTOR_MDC_CLASS_PAIR: {attribute: string; mdcClasses: string[]}[] = [
5964
{
@@ -94,13 +99,17 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
9499
_animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true});
95100

96101
private readonly _focusMonitor = inject(FocusMonitor);
102+
private _cleanupClick: (() => void) | undefined;
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,18 +162,36 @@ 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'});
167190

191+
if (this._isAnchor) {
192+
this._setupAsAnchor();
193+
}
194+
168195
// For each of the variant selectors that is present in the button's host
169196
// attributes, add the correct corresponding MDC classes.
170197
for (const {attribute, mdcClasses} of HOST_SELECTOR_MDC_CLASS_PAIR) {
@@ -179,6 +206,7 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
179206
}
180207

181208
ngOnDestroy() {
209+
this._cleanupClick?.();
182210
this._focusMonitor.stopMonitoring(this._elementRef);
183211
this._rippleLoader?.destroyRipple(this._elementRef.nativeElement);
184212
}
@@ -197,6 +225,10 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
197225
return this.ariaDisabled;
198226
}
199227

228+
if (this._isAnchor) {
229+
return this.disabled || null;
230+
}
231+
200232
return this.disabled && this.disabledInteractive ? true : null;
201233
}
202234

@@ -210,74 +242,41 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
210242
this.disableRipple || this.disabled,
211243
);
212244
}
213-
}
214245

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"',
246+
protected _getTabIndex() {
247+
if (this._isAnchor) {
248+
return this.disabled && !this.disabledInteractive ? -1 : this.tabIndex;
249+
}
250+
return this.tabIndex;
251+
}
223252

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-
};
253+
private _setupAsAnchor() {
254+
const renderer = inject(Renderer2);
255+
const initialTabIndex = inject(new HostAttributeToken('tabindex'), {optional: true});
238256

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;
246-
247-
@Input({
248-
transform: (value: unknown) => {
249-
return value == null ? undefined : numberAttribute(value);
250-
},
251-
})
252-
tabIndex: number;
257+
if (initialTabIndex !== null) {
258+
this.tabIndex = numberAttribute(initialTabIndex, undefined);
259+
}
253260

254-
ngOnInit(): void {
255261
this._ngZone.runOutsideAngular(() => {
256-
this._cleanupClick = this._renderer.listen(
262+
this._cleanupClick = renderer.listen(
257263
this._elementRef.nativeElement,
258264
'click',
259-
this._haltDisabledEvents,
265+
(event: Event) => {
266+
if (this.disabled) {
267+
event.preventDefault();
268+
event.stopImmediatePropagation();
269+
}
270+
},
260271
);
261272
});
262273
}
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-
}
283274
}
275+
276+
// tslint:disable:variable-name
277+
/**
278+
* Anchor button base.
279+
*/
280+
export const MatAnchorBase = MatButtonBase;
281+
export type MatAnchorBase = MatButtonBase;
282+
// 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)