From cd0f028738422aade36d53f81a25714490ba65c8 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 26 Dec 2023 08:44:11 -0800 Subject: [PATCH] fix(material/dialog): `mat-dialog-title` should work under `OnPush` `viewContainerRef` The `mat-dialog-title` directive updates state in a microtask and should call `ChangeDetectorRef.markForCheck`. Failing to do this will cause the component tree to not be checked if it lives under an `OnPush` component that has not otherwise been marked for check. --- .../dialog/dialog-content-directives.ts | 4 ++ src/material/dialog/dialog.spec.ts | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/material/dialog/dialog-content-directives.ts b/src/material/dialog/dialog-content-directives.ts index 254542bce6e9..d63d1e7d357b 100644 --- a/src/material/dialog/dialog-content-directives.ts +++ b/src/material/dialog/dialog-content-directives.ts @@ -7,8 +7,10 @@ */ import { + ChangeDetectorRef, Directive, ElementRef, + inject, Input, OnChanges, OnDestroy, @@ -102,6 +104,7 @@ export class MatDialogClose implements OnInit, OnChanges { }) export class MatDialogTitle implements OnInit, OnDestroy { @Input() id: string = `mat-mdc-dialog-title-${dialogElementUid++}`; + private readonly changeDetectorRef = inject(ChangeDetectorRef); constructor( // The dialog title directive is always used in combination with a `MatDialogRef`. @@ -121,6 +124,7 @@ export class MatDialogTitle implements OnInit, OnDestroy { // Note: we null check the queue, because there are some internal // tests that are mocking out `MatDialogRef` incorrectly. this._dialogRef._containerInstance?._ariaLabelledByQueue?.push(this.id); + this.changeDetectorRef.markForCheck(); }); } } diff --git a/src/material/dialog/dialog.spec.ts b/src/material/dialog/dialog.spec.ts index e8be88bc3231..84c49b2b87f1 100644 --- a/src/material/dialog/dialog.spec.ts +++ b/src/material/dialog/dialog.spec.ts @@ -49,6 +49,7 @@ import { MatDialogRef, MAT_DIALOG_DATA, MAT_DIALOG_DEFAULT_OPTIONS, + MatDialogTitle, } from './index'; import {CLOSE_ANIMATION_DURATION, OPEN_ANIMATION_DURATION} from './dialog-container'; @@ -1617,6 +1618,55 @@ describe('MDC-based MatDialog', () => { runContentElementTests(); }); + it('should set the aria-labelledby attribute to the id of the title under OnPush host', fakeAsync(() => { + @Component({ + standalone: true, + imports: [MatDialogTitle], + template: `

This is the first title

`, + }) + class DialogCmp {} + + @Component({ + template: '', + selector: 'child', + standalone: true, + }) + class Child { + constructor( + readonly viewContainerRef: ViewContainerRef, + readonly dialog: MatDialog, + ) {} + + open() { + this.dialog.open(DialogCmp, {viewContainerRef: this.viewContainerRef}); + } + } + + @Component({ + standalone: true, + imports: [Child], + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + }) + class OnPushHost { + @ViewChild(Child, {static: true}) child: Child; + } + + const hostFixture = TestBed.createComponent(OnPushHost); + hostFixture.componentInstance.child.open(); + hostFixture.autoDetectChanges(); + flush(); + + const overlayContainer = TestBed.inject(OverlayContainer); + const title = overlayContainer.getContainerElement().querySelector('[mat-dialog-title]')!; + const container = overlayContainerElement.querySelector('mat-dialog-container')!; + + expect(title.id).withContext('Expected title element to have an id.').toBeTruthy(); + expect(container.getAttribute('aria-labelledby')) + .withContext('Expected the aria-labelledby to match the title id.') + .toBe(title.id); + })); + function runContentElementTests() { it('should close the dialog when clicking on the close button', fakeAsync(() => { expect(overlayContainerElement.querySelectorAll('.mat-mdc-dialog-container').length).toBe(