@@ -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' ;
2525import { _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. */
5863const 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
0 commit comments