@@ -19,7 +19,6 @@ import {
1919 NgZone ,
2020 numberAttribute ,
2121 OnDestroy ,
22- OnInit ,
2322 Renderer2 ,
2423} from '@angular/core' ;
2524import { _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. */
5862const 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
0 commit comments