diff --git a/src/material/button/button-base.ts b/src/material/button/button-base.ts index 6c66a2304273..f66ab12627e7 100644 --- a/src/material/button/button-base.ts +++ b/src/material/button/button-base.ts @@ -13,13 +13,13 @@ import { booleanAttribute, Directive, ElementRef, + HostAttributeToken, inject, InjectionToken, Input, NgZone, numberAttribute, OnDestroy, - OnInit, Renderer2, } from '@angular/core'; import {_StructuralStylesLoader, MatRippleLoader, ThemePalette} from '@angular/material/core'; @@ -52,6 +52,7 @@ export const MAT_BUTTON_HOST = { // wants to target all Material buttons. '[class.mat-mdc-button-base]': 'true', '[class]': 'color ? "mat-" + color : ""', + '[attr.tabindex]': '_getTabIndex()', }; /** List of classes to add to buttons instances based on host attribute selector. */ @@ -94,6 +95,7 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { _animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true}); private readonly _focusMonitor = inject(FocusMonitor); + private _cleanupClick: (() => void) | undefined; /** * Handles the lazy creation of the MatButton ripple. @@ -101,6 +103,9 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { */ protected _rippleLoader: MatRippleLoader = inject(MatRippleLoader); + /** Whether the button is set on an anchor node. */ + protected _isAnchor: boolean; + /** Whether this button is a FAB. Used to apply the correct class on the ripple. */ protected _isFab = false; @@ -153,18 +158,28 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { @Input({transform: booleanAttribute}) disabledInteractive: boolean; + @Input({ + transform: (value: unknown) => (value == null ? undefined : numberAttribute(value)), + }) + tabIndex: number; + constructor(...args: unknown[]); constructor() { inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader); const config = inject(MAT_BUTTON_CONFIG, {optional: true}); - const element = this._elementRef.nativeElement; + const element: HTMLElement = this._elementRef.nativeElement; const classList = (element as HTMLElement).classList; + this._isAnchor = element.tagName === 'A'; this.disabledInteractive = config?.disabledInteractive ?? false; this.color = config?.color ?? null; this._rippleLoader?.configureRipple(element, {className: 'mat-mdc-button-ripple'}); + if (this._isAnchor) { + this._setupAsAnchor(); + } + // For each of the variant selectors that is present in the button's host // attributes, add the correct corresponding MDC classes. for (const {attribute, mdcClasses} of HOST_SELECTOR_MDC_CLASS_PAIR) { @@ -179,6 +194,7 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { } ngOnDestroy() { + this._cleanupClick?.(); this._focusMonitor.stopMonitoring(this._elementRef); this._rippleLoader?.destroyRipple(this._elementRef.nativeElement); } @@ -193,10 +209,9 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { } protected _getAriaDisabled() { - if (this.ariaDisabled != null) { - return this.ariaDisabled; + if (this._isAnchor) { + return this.ariaDisabled != null ? this.ariaDisabled : this.disabled || null; } - return this.disabled && this.disabledInteractive ? true : null; } @@ -210,74 +225,40 @@ export class MatButtonBase implements AfterViewInit, OnDestroy { this.disableRipple || this.disabled, ); } -} - -/** Shared host configuration for buttons using the `` tag. */ -export const MAT_ANCHOR_HOST = { - // Note that this is basically a noop on anchors, - // but it appears that some internal apps depend on it. - '[attr.disabled]': '_getDisabledAttribute()', - '[class.mat-mdc-button-disabled]': 'disabled', - '[class.mat-mdc-button-disabled-interactive]': 'disabledInteractive', - '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"', - // Note that we ignore the user-specified tabindex when it's disabled for - // consistency with the `mat-button` applied on native buttons where even - // though they have an index, they're not tabbable. - '[attr.tabindex]': 'disabled && !disabledInteractive ? -1 : tabIndex', - '[attr.aria-disabled]': '_getAriaDisabled()', - // MDC automatically applies the primary theme color to the button, but we want to support - // an unthemed version. If color is undefined, apply a CSS class that makes it easy to - // select and style this "theme". - '[class.mat-unthemed]': '!color', - // Add a class that applies to all buttons. This makes it easier to target if somebody - // wants to target all Material buttons. - '[class.mat-mdc-button-base]': 'true', - '[class]': 'color ? "mat-" + color : ""', -}; + protected _getTabIndex() { + if (this._isAnchor) { + return this.disabled && !this.disabledInteractive ? -1 : this.tabIndex; + } + return this.tabIndex; + } -/** - * Anchor button base. - */ -@Directive() -export class MatAnchorBase extends MatButtonBase implements OnInit, OnDestroy { - private _renderer = inject(Renderer2); - private _cleanupClick: () => void; + private _setupAsAnchor() { + const renderer = inject(Renderer2); + const initialTabIndex = inject(new HostAttributeToken('tabindex'), {optional: true}); - @Input({ - transform: (value: unknown) => { - return value == null ? undefined : numberAttribute(value); - }, - }) - tabIndex: number; + if (initialTabIndex !== null) { + this.tabIndex = numberAttribute(initialTabIndex, undefined); + } - ngOnInit(): void { this._ngZone.runOutsideAngular(() => { - this._cleanupClick = this._renderer.listen( + this._cleanupClick = renderer.listen( this._elementRef.nativeElement, 'click', - this._haltDisabledEvents, + (event: Event) => { + if (this.disabled) { + event.preventDefault(); + event.stopImmediatePropagation(); + } + }, ); }); } - - override ngOnDestroy(): void { - super.ngOnDestroy(); - this._cleanupClick?.(); - } - - _haltDisabledEvents = (event: Event): void => { - // A disabled button shouldn't apply any actions - if (this.disabled) { - event.preventDefault(); - event.stopImmediatePropagation(); - } - }; - - protected override _getAriaDisabled() { - if (this.ariaDisabled != null) { - return this.ariaDisabled; - } - return this.disabled || null; - } } + +// tslint:disable:variable-name +/** + * Anchor button base. + */ +export const MatAnchorBase = MatButtonBase; +// tslint:enable:variable-name diff --git a/src/material/button/button.ts b/src/material/button/button.ts index 938eb8380f24..72014f4007de 100644 --- a/src/material/button/button.ts +++ b/src/material/button/button.ts @@ -7,7 +7,7 @@ */ import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; -import {MAT_ANCHOR_HOST, MAT_BUTTON_HOST, MatAnchorBase, MatButtonBase} from './button-base'; +import {MAT_BUTTON_HOST, MatButtonBase} from './button-base'; /** * 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 './ @Component({ selector: ` button[mat-button], button[mat-raised-button], button[mat-flat-button], - button[mat-stroked-button] + button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], + a[mat-stroked-button] `, templateUrl: 'button.html', styleUrls: ['button.css', 'button-high-contrast.css'], host: MAT_BUTTON_HOST, - exportAs: 'matButton', + exportAs: 'matButton, matAnchor', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) export class MatButton extends MatButtonBase {} +// tslint:disable:variable-name /** * Material Design button component for anchor elements. Anchor elements are used to provide * links for the user to navigate across different routes or pages. @@ -42,13 +44,5 @@ export class MatButton extends MatButtonBase {} * specification. `MatAnchor` additionally captures an additional "flat" appearance, which matches * "contained" but without elevation. */ -@Component({ - selector: `a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button]`, - exportAs: 'matButton, matAnchor', - host: MAT_ANCHOR_HOST, - templateUrl: 'button.html', - styleUrls: ['button.css', 'button-high-contrast.css'], - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MatAnchor extends MatAnchorBase {} +export const MatAnchor = MatButton; +// tslint:enable:variable-name diff --git a/src/material/button/fab.ts b/src/material/button/fab.ts index a9edbcd256af..cdc9f00b81b2 100644 --- a/src/material/button/fab.ts +++ b/src/material/button/fab.ts @@ -16,8 +16,7 @@ import { inject, } from '@angular/core'; -import {MatAnchor} from './button'; -import {MAT_ANCHOR_HOST, MAT_BUTTON_HOST, MatButtonBase} from './button-base'; +import {MAT_BUTTON_HOST, MatButtonBase} from './button-base'; import {ThemePalette} from '@angular/material/core'; /** Default FAB options that can be overridden. */ @@ -60,7 +59,7 @@ const defaults = MAT_FAB_DEFAULT_OPTIONS_FACTORY(); * The `MatFabButton` class has two appearances: normal and extended. */ @Component({ - selector: `button[mat-fab]`, + selector: `button[mat-fab], a[mat-fab]`, templateUrl: 'button.html', styleUrl: 'fab.css', host: { @@ -68,7 +67,7 @@ const defaults = MAT_FAB_DEFAULT_OPTIONS_FACTORY(); '[class.mdc-fab--extended]': 'extended', '[class.mat-mdc-extended-fab]': 'extended', }, - exportAs: 'matButton', + exportAs: 'matButton, matAnchor', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -94,11 +93,11 @@ export class MatFabButton extends MatButtonBase { * See https://material.io/components/buttons-floating-action-button/ */ @Component({ - selector: `button[mat-mini-fab]`, + selector: `button[mat-mini-fab], a[mat-mini-fab]`, templateUrl: 'button.html', styleUrl: 'fab.css', host: MAT_BUTTON_HOST, - exportAs: 'matButton', + exportAs: 'matButton, matAnchor', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -116,6 +115,7 @@ export class MatMiniFabButton extends MatButtonBase { } } +// tslint:disable:variable-name /** * Material Design floating action button (FAB) component for anchor elements. Anchor elements * are used to provide links for the user to navigate across different routes or pages. @@ -123,59 +123,12 @@ export class MatMiniFabButton extends MatButtonBase { * * The `MatFabAnchor` class has two appearances: normal and extended. */ -@Component({ - selector: `a[mat-fab]`, - templateUrl: 'button.html', - styleUrl: 'fab.css', - host: { - ...MAT_ANCHOR_HOST, - '[class.mdc-fab--extended]': 'extended', - '[class.mat-mdc-extended-fab]': 'extended', - }, - exportAs: 'matButton, matAnchor', - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MatFabAnchor extends MatAnchor { - private _options = inject(MAT_FAB_DEFAULT_OPTIONS, {optional: true}); - - override _isFab = true; - - @Input({transform: booleanAttribute}) extended: boolean; - - constructor(...args: unknown[]); - - constructor() { - super(); - this._options = this._options || defaults; - this.color = this._options!.color || defaults.color; - } -} +export const MatFabAnchor = MatFabButton; /** * Material Design mini floating action button (FAB) component for anchor elements. Anchor elements * are used to provide links for the user to navigate across different routes or pages. * See https://material.io/components/buttons-floating-action-button/ */ -@Component({ - selector: `a[mat-mini-fab]`, - templateUrl: 'button.html', - styleUrl: 'fab.css', - host: MAT_ANCHOR_HOST, - exportAs: 'matButton, matAnchor', - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MatMiniFabAnchor extends MatAnchor { - private _options = inject(MAT_FAB_DEFAULT_OPTIONS, {optional: true}); - - override _isFab = true; - - constructor(...args: unknown[]); - - constructor() { - super(); - this._options = this._options || defaults; - this.color = this._options!.color || defaults.color; - } -} +export const MatMiniFabAnchor = MatMiniFabButton; +// tslint:enable:variable-name diff --git a/src/material/button/icon-button.ts b/src/material/button/icon-button.ts index 419383ca1180..b407136ad02b 100644 --- a/src/material/button/icon-button.ts +++ b/src/material/button/icon-button.ts @@ -7,7 +7,7 @@ */ import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core'; -import {MAT_ANCHOR_HOST, MAT_BUTTON_HOST, MatAnchorBase, MatButtonBase} from './button-base'; +import {MAT_BUTTON_HOST, MatButtonBase} from './button-base'; /** * Material Design icon button component. This type of button displays a single interactive icon for @@ -15,11 +15,11 @@ import {MAT_ANCHOR_HOST, MAT_BUTTON_HOST, MatAnchorBase, MatButtonBase} from './ * See https://material.io/develop/web/components/buttons/icon-buttons/ */ @Component({ - selector: `button[mat-icon-button]`, + selector: `button[mat-icon-button], a[mat-icon-button]`, templateUrl: 'icon-button.html', styleUrls: ['icon-button.css', 'button-high-contrast.css'], host: MAT_BUTTON_HOST, - exportAs: 'matButton', + exportAs: 'matButton, matAnchor', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -32,18 +32,11 @@ export class MatIconButton extends MatButtonBase { } } +// tslint:disable:variable-name /** * Material Design icon button component for anchor elements. This button displays a single * interaction icon that allows users to navigate across different routes or pages. * See https://material.io/develop/web/components/buttons/icon-buttons/ */ -@Component({ - selector: `a[mat-icon-button]`, - templateUrl: 'icon-button.html', - styleUrls: ['icon-button.css', 'button-high-contrast.css'], - host: MAT_ANCHOR_HOST, - exportAs: 'matButton, matAnchor', - encapsulation: ViewEncapsulation.None, - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class MatIconAnchor extends MatAnchorBase {} +export const MatIconAnchor = MatIconButton; +// tslint:enable:variable-name diff --git a/src/material/button/module.ts b/src/material/button/module.ts index 24185f18b865..e5e5bd99e495 100644 --- a/src/material/button/module.ts +++ b/src/material/button/module.ts @@ -8,33 +8,19 @@ import {NgModule} from '@angular/core'; import {MatCommonModule, MatRippleModule} from '@angular/material/core'; -import {MatAnchor, MatButton} from './button'; -import {MatFabAnchor, MatFabButton, MatMiniFabAnchor, MatMiniFabButton} from './fab'; -import {MatIconAnchor, MatIconButton} from './icon-button'; +import {MatButton} from './button'; +import {MatFabButton, MatMiniFabButton} from './fab'; +import {MatIconButton} from './icon-button'; @NgModule({ imports: [ MatCommonModule, MatRippleModule, - MatAnchor, MatButton, - MatIconAnchor, - MatMiniFabAnchor, MatMiniFabButton, MatIconButton, - MatFabAnchor, MatFabButton, ], - exports: [ - MatAnchor, - MatButton, - MatIconAnchor, - MatIconButton, - MatMiniFabAnchor, - MatMiniFabButton, - MatFabAnchor, - MatFabButton, - MatCommonModule, - ], + exports: [MatCommonModule, MatButton, MatMiniFabButton, MatIconButton, MatFabButton], }) export class MatButtonModule {} diff --git a/src/material/datepicker/datepicker-toggle.html b/src/material/datepicker/datepicker-toggle.html index a1f7f190dfcf..af60fb5dad76 100644 --- a/src/material/datepicker/datepicker-toggle.html +++ b/src/material/datepicker/datepicker-toggle.html @@ -4,7 +4,7 @@ type="button" [attr.aria-haspopup]="datepicker ? 'dialog' : null" [attr.aria-label]="ariaLabel || _intl.openCalendarLabel" - [attr.tabindex]="disabled ? -1 : tabIndex" + [tabIndex]="disabled ? -1 : tabIndex" [attr.aria-expanded]="datepicker ? datepicker.opened : null" [disabled]="disabled" [disableRipple]="disableRipple"> diff --git a/src/material/timepicker/timepicker-toggle.html b/src/material/timepicker/timepicker-toggle.html index 879329c90dff..d023f065f91f 100644 --- a/src/material/timepicker/timepicker-toggle.html +++ b/src/material/timepicker/timepicker-toggle.html @@ -5,7 +5,7 @@ [attr.aria-label]="getAriaLabel()" [attr.aria-labelledby]="ariaLabelledby()" [attr.aria-expanded]="timepicker().isOpen()" - [attr.tabindex]="_isDisabled() ? -1 : tabIndex()" + [tabIndex]="_isDisabled() ? -1 : tabIndex()" [disabled]="_isDisabled()" [disableRipple]="disableRipple()"> diff --git a/tools/public_api_guard/material/button.md b/tools/public_api_guard/material/button.md index 1bd43539fc33..34eb662cb8d7 100644 --- a/tools/public_api_guard/material/button.md +++ b/tools/public_api_guard/material/button.md @@ -13,7 +13,6 @@ import { InjectionToken } from '@angular/core'; import { MatRippleLoader } from '@angular/material/core'; import { NgZone } from '@angular/core'; import { OnDestroy } from '@angular/core'; -import { OnInit } from '@angular/core'; import { ThemePalette } from '@angular/material/core'; // @public @@ -26,17 +25,12 @@ export const MAT_FAB_DEFAULT_OPTIONS: InjectionToken; export function MAT_FAB_DEFAULT_OPTIONS_FACTORY(): MatFabDefaultOptions; // @public -export class MatAnchor extends MatAnchorBase { - // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; - // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; -} +export const MatAnchor: typeof MatButton; // @public export class MatButton extends MatButtonBase { // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -54,23 +48,11 @@ export class MatButtonModule { // (undocumented) static ɵinj: i0.ɵɵInjectorDeclaration; // (undocumented) - static ɵmod: i0.ɵɵNgModuleDeclaration; + static ɵmod: i0.ɵɵNgModuleDeclaration; } // @public -export class MatFabAnchor extends MatAnchor { - constructor(...args: unknown[]); - // (undocumented) - extended: boolean; - // (undocumented) - _isFab: boolean; - // (undocumented) - static ngAcceptInputType_extended: unknown; - // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; - // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; -} +export const MatFabAnchor: typeof MatFabButton; // @public export class MatFabButton extends MatButtonBase { @@ -82,7 +64,7 @@ export class MatFabButton extends MatButtonBase { // (undocumented) static ngAcceptInputType_extended: unknown; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -93,32 +75,19 @@ export interface MatFabDefaultOptions { } // @public -export class MatIconAnchor extends MatAnchorBase { - // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; - // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; -} +export const MatIconAnchor: typeof MatIconButton; // @public export class MatIconButton extends MatButtonBase { constructor(...args: unknown[]); // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } // @public -export class MatMiniFabAnchor extends MatAnchor { - constructor(...args: unknown[]); - // (undocumented) - _isFab: boolean; - // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; - // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; -} +export const MatMiniFabAnchor: typeof MatMiniFabButton; // @public export class MatMiniFabButton extends MatButtonBase { @@ -126,7 +95,7 @@ export class MatMiniFabButton extends MatButtonBase { // (undocumented) _isFab: boolean; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }