/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {FocusMonitor, FocusOrigin} from '@angular/cdk/a11y'; import {Platform} from '@angular/cdk/platform'; import { AfterViewInit, ANIMATION_MODULE_TYPE, booleanAttribute, Directive, ElementRef, inject, InjectionToken, Input, NgZone, numberAttribute, OnDestroy, OnInit, } from '@angular/core'; import {_StructuralStylesLoader, MatRippleLoader, ThemePalette} from '@angular/material/core'; import {_CdkPrivateStyleLoader} from '@angular/cdk/private'; /** Object that can be used to configure the default options for the button component. */ export interface MatButtonConfig { /** Whether disabled buttons should be interactive. */ disabledInteractive?: boolean; /** Default palette color to apply to buttons. */ color?: ThemePalette; } /** Injection token that can be used to provide the default options the button component. */ export const MAT_BUTTON_CONFIG = new InjectionToken('MAT_BUTTON_CONFIG'); /** Shared host configuration for all buttons */ export const MAT_BUTTON_HOST = { '[attr.disabled]': '_getDisabledAttribute()', '[attr.aria-disabled]': '_getAriaDisabled()', '[class.mat-mdc-button-disabled]': 'disabled', '[class.mat-mdc-button-disabled-interactive]': 'disabledInteractive', '[class._mat-animation-noopable]': '_animationMode === "NoopAnimations"', // 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 : ""', }; /** List of classes to add to buttons instances based on host attribute selector. */ const HOST_SELECTOR_MDC_CLASS_PAIR: {attribute: string; mdcClasses: string[]}[] = [ { attribute: 'mat-button', mdcClasses: ['mdc-button', 'mat-mdc-button'], }, { attribute: 'mat-flat-button', mdcClasses: ['mdc-button', 'mdc-button--unelevated', 'mat-mdc-unelevated-button'], }, { attribute: 'mat-raised-button', mdcClasses: ['mdc-button', 'mdc-button--raised', 'mat-mdc-raised-button'], }, { attribute: 'mat-stroked-button', mdcClasses: ['mdc-button', 'mdc-button--outlined', 'mat-mdc-outlined-button'], }, { attribute: 'mat-fab', mdcClasses: ['mdc-fab', 'mat-mdc-fab-base', 'mat-mdc-fab'], }, { attribute: 'mat-mini-fab', mdcClasses: ['mdc-fab', 'mat-mdc-fab-base', 'mdc-fab--mini', 'mat-mdc-mini-fab'], }, { attribute: 'mat-icon-button', mdcClasses: ['mdc-icon-button', 'mat-mdc-icon-button'], }, ]; /** Base class for all buttons. */ @Directive() export class MatButtonBase implements AfterViewInit, OnDestroy { _elementRef = inject(ElementRef); _platform = inject(Platform); _ngZone = inject(NgZone); _animationMode = inject(ANIMATION_MODULE_TYPE, {optional: true}); private readonly _focusMonitor = inject(FocusMonitor); /** * Handles the lazy creation of the MatButton ripple. * Used to improve initial load time of large applications. */ protected _rippleLoader: MatRippleLoader = inject(MatRippleLoader); /** Whether this button is a FAB. Used to apply the correct class on the ripple. */ protected _isFab = false; /** * Theme color of the button. This API is supported in M2 themes only, it has * no effect in M3 themes. * * For information on applying color variants in M3, see * https://material.angular.io/guide/theming#using-component-color-variants. */ @Input() color?: string | null; /** Whether the ripple effect is disabled or not. */ @Input({transform: booleanAttribute}) get disableRipple(): boolean { return this._disableRipple; } set disableRipple(value: any) { this._disableRipple = value; this._updateRippleDisabled(); } private _disableRipple: boolean = false; /** Whether the button is disabled. */ @Input({transform: booleanAttribute}) get disabled(): boolean { return this._disabled; } set disabled(value: any) { this._disabled = value; this._updateRippleDisabled(); } private _disabled: boolean = false; /** `aria-disabled` value of the button. */ @Input({transform: booleanAttribute, alias: 'aria-disabled'}) ariaDisabled: boolean | undefined; /** * Natively disabled buttons prevent focus and any pointer events from reaching the button. * In some scenarios this might not be desirable, because it can prevent users from finding out * why the button is disabled (e.g. via tooltip). * * Enabling this input will change the button so that it is styled to be disabled and will be * marked as `aria-disabled`, but it will allow the button to receive events and focus. * * Note that by enabling this, you need to set the `tabindex` yourself if the button isn't * meant to be tabbable and you have to prevent the button action (e.g. form submissions). */ @Input({transform: booleanAttribute}) disabledInteractive: boolean; constructor(...args: unknown[]); constructor() { inject(_CdkPrivateStyleLoader).load(_StructuralStylesLoader); const config = inject(MAT_BUTTON_CONFIG, {optional: true}); const element = this._elementRef.nativeElement; const classList = (element as HTMLElement).classList; this.disabledInteractive = config?.disabledInteractive ?? false; this.color = config?.color ?? null; this._rippleLoader?.configureRipple(element, {className: 'mat-mdc-button-ripple'}); // 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) { if (element.hasAttribute(attribute)) { classList.add(...mdcClasses); } } } ngAfterViewInit() { this._focusMonitor.monitor(this._elementRef, true); } ngOnDestroy() { this._focusMonitor.stopMonitoring(this._elementRef); this._rippleLoader?.destroyRipple(this._elementRef.nativeElement); } /** Focuses the button. */ focus(origin: FocusOrigin = 'program', options?: FocusOptions): void { if (origin) { this._focusMonitor.focusVia(this._elementRef.nativeElement, origin, options); } else { this._elementRef.nativeElement.focus(options); } } protected _getAriaDisabled() { if (this.ariaDisabled != null) { return this.ariaDisabled; } return this.disabled && this.disabledInteractive ? true : null; } protected _getDisabledAttribute() { return this.disabledInteractive || !this.disabled ? null : true; } private _updateRippleDisabled(): void { this._rippleLoader?.setDisabled( this._elementRef.nativeElement, 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 : ""', }; /** * Anchor button base. */ @Directive() export class MatAnchorBase extends MatButtonBase implements OnInit, OnDestroy { @Input({ transform: (value: unknown) => { return value == null ? undefined : numberAttribute(value); }, }) tabIndex: number; ngOnInit(): void { this._ngZone.runOutsideAngular(() => { this._elementRef.nativeElement.addEventListener('click', this._haltDisabledEvents); }); } override ngOnDestroy(): void { super.ngOnDestroy(); this._elementRef.nativeElement.removeEventListener('click', this._haltDisabledEvents); } _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; } }