From df59bfcb86aa3d6f1c5a8de405ccfb18397a0375 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Tue, 21 Jul 2026 13:01:59 +0300 Subject: [PATCH 1/2] fix(dialog): render asynchronous open state in zoneless apps --- .../src/dialog/dialog.component.spec.ts | 58 ++++++++++++++++++- .../dialog/src/dialog/dialog.component.ts | 4 +- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/dialog/src/dialog/dialog.component.spec.ts b/projects/igniteui-angular/dialog/src/dialog/dialog.component.spec.ts index 64edc2735ec..83f13301e0f 100644 --- a/projects/igniteui-angular/dialog/src/dialog/dialog.component.spec.ts +++ b/projects/igniteui-angular/dialog/src/dialog/dialog.component.spec.ts @@ -1,4 +1,4 @@ -import { Component, ViewChild, ChangeDetectionStrategy } from '@angular/core'; +import { Component, ViewChild, ChangeDetectionStrategy, provideZonelessChangeDetection, signal } from '@angular/core'; import { TestBed, fakeAsync, tick, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -691,3 +691,59 @@ class PositionSettingsDialogComponent { }; } + +@Component({ + template: ` + `, + changeDetection: ChangeDetectionStrategy.Eager, + imports: [IgxDialogComponent] +}) +class ZonelessDialogHostComponent { + @ViewChild('dialog', { static: true }) public dialog: IgxDialogComponent; + public readonly open = signal(false); +} + +describe('Dialog - zoneless change detection', () => { + const DIALOG_WINDOW = '.igx-dialog__window'; + + const nextFrame = () => new Promise((resolve) => requestAnimationFrame(() => resolve())); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, ZonelessDialogHostComponent], + providers: [provideZonelessChangeDetection()] + }).compileComponents(); + })); + + afterEach(() => { + UIInteractions.clearOverlay(); + }); + + it('renders the dialog when isOpen is set from an asynchronous callback', async () => { + const fixture = TestBed.createComponent(ZonelessDialogHostComponent); + fixture.detectChanges(); + + const host = fixture.debugElement.query(By.css('igx-dialog')).nativeElement as HTMLElement; + const dialog = fixture.componentInstance.dialog; + + await new Promise(resolve => { + setTimeout(() => { + fixture.componentInstance.open.set(true); + resolve(); + }); + }); + await fixture.whenStable(); + await nextFrame(); + await fixture.whenStable(); + + const dialogWindow = document.querySelector(DIALOG_WINDOW); + expect(dialog.isOpen).toBeTrue(); + expect(dialog.isCollapsed).toBeFalse(); + expect(dialogWindow).withContext('dialog window is in the DOM').not.toBeNull(); + expect(dialogWindow.getClientRects().length) + .withContext('dialog window is actually rendered').toBeGreaterThan(0); + expect(host.classList.contains('igx-dialog--hidden')) + .withContext('host class mirrors isCollapsed') + .toBeFalse(); + }); +}); diff --git a/projects/igniteui-angular/dialog/src/dialog/dialog.component.ts b/projects/igniteui-angular/dialog/src/dialog/dialog.component.ts index 1761eb9248f..d9e8902f220 100644 --- a/projects/igniteui-angular/dialog/src/dialog/dialog.component.ts +++ b/projects/igniteui-angular/dialog/src/dialog/dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, EventEmitter, HostBinding, Input, OnDestroy, OnInit, Output, ViewChild, AfterContentInit, booleanAttribute, inject, ChangeDetectionStrategy } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, HostBinding, Input, OnDestroy, OnInit, Output, ViewChild, AfterContentInit, booleanAttribute, inject } from '@angular/core'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { IgxNavigationService, IToggleView } from 'igniteui-angular/core'; @@ -46,6 +46,7 @@ let DIALOG_ID = 0; imports: [IgxToggleDirective, IgxFocusTrapDirective, IgxFocusDirective, IgxButtonDirective, IgxRippleDirective] }) export class IgxDialogComponent implements IToggleView, OnInit, OnDestroy, AfterContentInit { + private cdr = inject(ChangeDetectorRef); private elementRef = inject(ElementRef); private navService = inject(IgxNavigationService, { optional: true }); @@ -370,6 +371,7 @@ export class IgxDialogComponent implements IToggleView, OnInit, OnDestroy, After if (value) { requestAnimationFrame(() => { this.open(); + this.cdr.markForCheck(); }); } else { this.close(); From 8628784b13cf4f9f7b0108d64c2d4b81eb115d73 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Tue, 21 Jul 2026 14:27:04 +0300 Subject: [PATCH 2/2] fix(dialog): handle potential null for dialog window client rects --- .../igniteui-angular/dialog/src/dialog/dialog.component.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/dialog/src/dialog/dialog.component.spec.ts b/projects/igniteui-angular/dialog/src/dialog/dialog.component.spec.ts index 83f13301e0f..81acbb65d1b 100644 --- a/projects/igniteui-angular/dialog/src/dialog/dialog.component.spec.ts +++ b/projects/igniteui-angular/dialog/src/dialog/dialog.component.spec.ts @@ -740,7 +740,7 @@ describe('Dialog - zoneless change detection', () => { expect(dialog.isOpen).toBeTrue(); expect(dialog.isCollapsed).toBeFalse(); expect(dialogWindow).withContext('dialog window is in the DOM').not.toBeNull(); - expect(dialogWindow.getClientRects().length) + expect(dialogWindow?.getClientRects().length ?? 0) .withContext('dialog window is actually rendered').toBeGreaterThan(0); expect(host.classList.contains('igx-dialog--hidden')) .withContext('host class mirrors isCollapsed')