/** * @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 { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, inject, } from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {CdkScrollable} from '@angular/cdk/scrolling'; import {MatDialog} from './dialog'; import {_closeDialogVia, MatDialogRef} from './dialog-ref'; /** * Button that will close the current dialog. */ @Directive({ selector: '[mat-dialog-close], [matDialogClose]', exportAs: 'matDialogClose', host: { '(click)': '_onButtonClick($event)', '[attr.aria-label]': 'ariaLabel || null', '[attr.type]': 'type', }, }) export class MatDialogClose implements OnInit, OnChanges { dialogRef = inject>(MatDialogRef, {optional: true})!; private _elementRef = inject>(ElementRef); private _dialog = inject(MatDialog); /** Screen-reader label for the button. */ @Input('aria-label') ariaLabel: string; /** Default to "button" to prevents accidental form submits. */ @Input() type: 'submit' | 'button' | 'reset' = 'button'; /** Dialog close input. */ @Input('mat-dialog-close') dialogResult: any; @Input('matDialogClose') _matDialogClose: any; constructor(...args: unknown[]); constructor() {} ngOnInit() { if (!this.dialogRef) { // When this directive is included in a dialog via TemplateRef (rather than being // in a Component), the DialogRef isn't available via injection because embedded // views cannot be given a custom injector. Instead, we look up the DialogRef by // ID. This must occur in `onInit`, as the ID binding for the dialog container won't // be resolved at constructor time. this.dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!; } } ngOnChanges(changes: SimpleChanges) { const proxiedChange = changes['_matDialogClose'] || changes['_matDialogCloseResult']; if (proxiedChange) { this.dialogResult = proxiedChange.currentValue; } } _onButtonClick(event: MouseEvent) { // Determinate the focus origin using the click event, because using the FocusMonitor will // result in incorrect origins. Most of the time, close buttons will be auto focused in the // dialog, and therefore clicking the button won't result in a focus change. This means that // the FocusMonitor won't detect any origin change, and will always output `program`. _closeDialogVia( this.dialogRef, event.screenX === 0 && event.screenY === 0 ? 'keyboard' : 'mouse', this.dialogResult, ); } } @Directive() export abstract class MatDialogLayoutSection implements OnInit, OnDestroy { protected _dialogRef = inject>(MatDialogRef, {optional: true})!; private _elementRef = inject>(ElementRef); private _dialog = inject(MatDialog); constructor(...args: unknown[]); constructor() {} protected abstract _onAdd(): void; protected abstract _onRemove(): void; ngOnInit() { if (!this._dialogRef) { this._dialogRef = getClosestDialog(this._elementRef, this._dialog.openDialogs)!; } if (this._dialogRef) { Promise.resolve().then(() => { this._onAdd(); }); } } ngOnDestroy() { // Note: we null check because there are some internal // tests that are mocking out `MatDialogRef` incorrectly. const instance = this._dialogRef?._containerInstance; if (instance) { Promise.resolve().then(() => { this._onRemove(); }); } } } /** * Title of a dialog element. Stays fixed to the top of the dialog when scrolling. */ @Directive({ selector: '[mat-dialog-title], [matDialogTitle]', exportAs: 'matDialogTitle', host: { 'class': 'mat-mdc-dialog-title mdc-dialog__title', '[id]': 'id', }, }) export class MatDialogTitle extends MatDialogLayoutSection { @Input() id: string = inject(_IdGenerator).getId('mat-mdc-dialog-title-'); protected _onAdd() { // Note: we null check the queue, because there are some internal // tests that are mocking out `MatDialogRef` incorrectly. this._dialogRef._containerInstance?._addAriaLabelledBy?.(this.id); } protected override _onRemove(): void { this._dialogRef?._containerInstance?._removeAriaLabelledBy?.(this.id); } } /** * Scrollable content container of a dialog. */ @Directive({ selector: `[mat-dialog-content], mat-dialog-content, [matDialogContent]`, host: {'class': 'mat-mdc-dialog-content mdc-dialog__content'}, hostDirectives: [CdkScrollable], }) export class MatDialogContent {} /** * Container for the bottom action buttons in a dialog. * Stays fixed to the bottom when scrolling. */ @Directive({ selector: `[mat-dialog-actions], mat-dialog-actions, [matDialogActions]`, host: { 'class': 'mat-mdc-dialog-actions mdc-dialog__actions', '[class.mat-mdc-dialog-actions-align-start]': 'align === "start"', '[class.mat-mdc-dialog-actions-align-center]': 'align === "center"', '[class.mat-mdc-dialog-actions-align-end]': 'align === "end"', }, }) export class MatDialogActions extends MatDialogLayoutSection { /** * Horizontal alignment of action buttons. */ @Input() align?: 'start' | 'center' | 'end'; protected _onAdd() { this._dialogRef._containerInstance?._updateActionSectionCount?.(1); } protected override _onRemove(): void { this._dialogRef._containerInstance?._updateActionSectionCount?.(-1); } } /** * Finds the closest MatDialogRef to an element by looking at the DOM. * @param element Element relative to which to look for a dialog. * @param openDialogs References to the currently-open dialogs. */ function getClosestDialog(element: ElementRef, openDialogs: MatDialogRef[]) { let parent: HTMLElement | null = element.nativeElement.parentElement; while (parent && !parent.classList.contains('mat-mdc-dialog-container')) { parent = parent.parentElement; } return parent ? openDialogs.find(dialog => dialog.id === parent!.id) : null; }