@@ -13,13 +13,13 @@ import {
13
13
booleanAttribute ,
14
14
Directive ,
15
15
ElementRef ,
16
+ HostAttributeToken ,
16
17
inject ,
17
18
InjectionToken ,
18
19
Input ,
19
20
NgZone ,
20
21
numberAttribute ,
21
22
OnDestroy ,
22
- OnInit ,
23
23
Renderer2 ,
24
24
} from '@angular/core' ;
25
25
import { _StructuralStylesLoader , MatRippleLoader , ThemePalette } from '@angular/material/core' ;
@@ -52,8 +52,13 @@ export const MAT_BUTTON_HOST = {
52
52
// wants to target all Material buttons.
53
53
'[class.mat-mdc-button-base]' : 'true' ,
54
54
'[class]' : 'color ? "mat-" + color : ""' ,
55
+ '[attr.tabindex]' : '_getTabIndex()' ,
55
56
} ;
56
57
58
+ function transformTabIndex ( value : unknown ) : number | undefined {
59
+ return value == null ? undefined : numberAttribute ( value ) ;
60
+ }
61
+
57
62
/** List of classes to add to buttons instances based on host attribute selector. */
58
63
const HOST_SELECTOR_MDC_CLASS_PAIR : { attribute : string ; mdcClasses : string [ ] } [ ] = [
59
64
{
@@ -94,13 +99,17 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
94
99
_animationMode = inject ( ANIMATION_MODULE_TYPE , { optional : true } ) ;
95
100
96
101
private readonly _focusMonitor = inject ( FocusMonitor ) ;
102
+ private _cleanupClick : ( ( ) => void ) | undefined ;
97
103
98
104
/**
99
105
* Handles the lazy creation of the MatButton ripple.
100
106
* Used to improve initial load time of large applications.
101
107
*/
102
108
protected _rippleLoader : MatRippleLoader = inject ( MatRippleLoader ) ;
103
109
110
+ /** Whether the button is set on an anchor node. */
111
+ protected _isAnchor : boolean ;
112
+
104
113
/** Whether this button is a FAB. Used to apply the correct class on the ripple. */
105
114
protected _isFab = false ;
106
115
@@ -153,18 +162,36 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
153
162
@Input ( { transform : booleanAttribute } )
154
163
disabledInteractive : boolean ;
155
164
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
+
156
178
constructor ( ...args : unknown [ ] ) ;
157
179
158
180
constructor ( ) {
159
181
inject ( _CdkPrivateStyleLoader ) . load ( _StructuralStylesLoader ) ;
160
182
const config = inject ( MAT_BUTTON_CONFIG , { optional : true } ) ;
161
- const element = this . _elementRef . nativeElement ;
183
+ const element : HTMLElement = this . _elementRef . nativeElement ;
162
184
const classList = ( element as HTMLElement ) . classList ;
163
185
186
+ this . _isAnchor = element . tagName === 'A' ;
164
187
this . disabledInteractive = config ?. disabledInteractive ?? false ;
165
188
this . color = config ?. color ?? null ;
166
189
this . _rippleLoader ?. configureRipple ( element , { className : 'mat-mdc-button-ripple' } ) ;
167
190
191
+ if ( this . _isAnchor ) {
192
+ this . _setupAsAnchor ( ) ;
193
+ }
194
+
168
195
// For each of the variant selectors that is present in the button's host
169
196
// attributes, add the correct corresponding MDC classes.
170
197
for ( const { attribute, mdcClasses} of HOST_SELECTOR_MDC_CLASS_PAIR ) {
@@ -179,6 +206,7 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
179
206
}
180
207
181
208
ngOnDestroy ( ) {
209
+ this . _cleanupClick ?.( ) ;
182
210
this . _focusMonitor . stopMonitoring ( this . _elementRef ) ;
183
211
this . _rippleLoader ?. destroyRipple ( this . _elementRef . nativeElement ) ;
184
212
}
@@ -197,6 +225,10 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
197
225
return this . ariaDisabled ;
198
226
}
199
227
228
+ if ( this . _isAnchor ) {
229
+ return this . disabled || null ;
230
+ }
231
+
200
232
return this . disabled && this . disabledInteractive ? true : null ;
201
233
}
202
234
@@ -210,74 +242,41 @@ export class MatButtonBase implements AfterViewInit, OnDestroy {
210
242
this . disableRipple || this . disabled ,
211
243
) ;
212
244
}
213
- }
214
245
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
+ }
223
252
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 } ) ;
238
256
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
+ }
253
260
254
- ngOnInit ( ) : void {
255
261
this . _ngZone . runOutsideAngular ( ( ) => {
256
- this . _cleanupClick = this . _renderer . listen (
262
+ this . _cleanupClick = renderer . listen (
257
263
this . _elementRef . nativeElement ,
258
264
'click' ,
259
- this . _haltDisabledEvents ,
265
+ ( event : Event ) => {
266
+ if ( this . disabled ) {
267
+ event . preventDefault ( ) ;
268
+ event . stopImmediatePropagation ( ) ;
269
+ }
270
+ } ,
260
271
) ;
261
272
} ) ;
262
273
}
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
274
}
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
0 commit comments