diff --git a/CHANGELOG.md b/CHANGELOG.md index bf83fc33a69..1d82eb63f33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,8 +90,10 @@ All notable changes for each version of this project will be documented in this - Opening the calendar from the default icon no longer briefly selects the input value in the `inputFormat` (e.g. `09/15/2026`). As with `igx-picker-toggle`, the icon click no longer propagates to the picker's ancestors. - The calendar receives the focus as soon as it starts opening instead of after the opening animation. - The default browser action of `Alt + Arrow Down` and `Alt + Arrow Up`, which open and close the calendar, is now prevented. + - Destroying the picker while its calendar is open no longer logs `igxOverlay.detach was called with wrong id` in the console. In the grid, this happened whenever a date cell edit ended while the calendar was open. - `IgxDateRangePickerComponent` - `Alt + Arrow Down` and `Alt + Arrow Up`, which open and close the calendar, no longer scroll the page. Previously their default browser action was not prevented. + - Destroying the picker while its calendar is open no longer logs `igxOverlay.detach was called with wrong id` in the console. - `IgxDragDirective` - `transitionToOrigin()` with a start location equal to the origin location no longer animates. Previously the vertical start position was compared with the horizontal origin. - `transitionToOrigin()` and `transitionTo()` now emit `transitioned` right away when the element is already at the target or there is no ghost to animate (e.g. its creation was canceled in `ghostCreate`). Previously no `transitioned` was emitted and the next drag did not start correctly. diff --git a/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.spec.ts b/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.spec.ts index c7b21ba69e4..33b28ed5cc9 100644 --- a/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.spec.ts +++ b/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.spec.ts @@ -1089,7 +1089,8 @@ describe('IgxDatePicker', () => { this.closed.emit(mockOverlayEventArgs); }), detach: vi.fn(), - attach: vi.fn().mockReturnValue(mockOverlayId) + attach: vi.fn().mockReturnValue(mockOverlayId), + getOverlayById: vi.fn().mockReturnValue({ id: mockOverlayId }) } as any; mockDateEditor = { @@ -1750,6 +1751,40 @@ describe('IgxDatePicker', () => { }); }); + describe('Destroy', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, IgxDatePickerConditionalComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(IgxDatePickerConditionalComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it.each([PickerInteractionMode.DropDown, PickerInteractionMode.Dialog])( + 'should not warn about a wrong overlay id when destroyed while open in %s mode - issue #17938', async (mode) => { + fixture.componentInstance.mode.set(mode); + fixture.detectChanges(); + const warnSpy = vi.spyOn(console, 'warn'); + + fixture.componentInstance.datePicker()!.open(); + await fixture.whenStable(); + expect(fixture.componentInstance.datePicker()!.collapsed).toBe(false); + expect(document.querySelector('igx-calendar-container')).not.toBeNull(); + + fixture.componentInstance.showPicker.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentInstance.datePicker()).toBeUndefined(); + expect(document.querySelector('igx-calendar-container')).toBeNull(); + expect(warnSpy).not.toHaveBeenCalledWith('igxOverlay.detach was called with wrong id: ', expect.anything()); + }); + }); + describe('Zoneless', () => { let fixture: ComponentFixture; let datePicker: IgxDatePickerComponent; @@ -1857,6 +1892,19 @@ export class IgxDatePickerTestComponent { public maxValue: string | Date; } +@Component({ + template: ` + @if (showPicker()) { + + }`, + imports: [IgxDatePickerComponent] +}) +export class IgxDatePickerConditionalComponent { + public datePicker = viewChild(IgxDatePickerComponent); + public showPicker = signal(true); + public mode = signal(PickerInteractionMode.DropDown); +} + @Component({ template: ` diff --git a/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.ts b/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.ts index a09da174dab..69d8e5b8d42 100644 --- a/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.ts +++ b/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.ts @@ -803,10 +803,12 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr if (this._statusChanges$) { this._statusChanges$.unsubscribe(); } - if (this._overlayId) { + // The overlay service detaches the calendar container on its own when Angular destroys it, + // which happens before this hook when the picker is destroyed with its calendar open. + if (this._overlayId && this._overlayService.getOverlayById(this._overlayId)) { this._overlayService.detach(this._overlayId); - this._overlayId = ''; } + this._overlayId = ''; } /** @hidden @internal */ diff --git a/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.spec.ts b/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.spec.ts index 10568830daa..f4e150083f3 100644 --- a/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.spec.ts +++ b/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.spec.ts @@ -1,5 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Component, OnInit, ViewChild, DebugElement, ChangeDetectionStrategy, inject, ElementRef, signal, provideZonelessChangeDetection } from '@angular/core'; +import { Component, OnInit, ViewChild, DebugElement, ChangeDetectionStrategy, inject, ElementRef, signal, provideZonelessChangeDetection, viewChild } from '@angular/core'; import { IgxInputDirective, IgxInputGroupComponent, IgxInputState, IgxLabelDirective, IgxPrefixDirective, IgxSuffixDirective } from '../../../input-group/src/public_api'; import { CustomDateRange, DateRange, PickerCalendarOrientation, PickerHeaderOrientation, PickerInteractionMode } from '../../../core/src/date-common/types'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; @@ -2405,6 +2405,41 @@ describe('IgxDateRangePicker', () => { }); }); + describe('Destroy', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + imports: [NoopAnimationsModule, DateRangeConditionalComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(DateRangeConditionalComponent); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it.each([PickerInteractionMode.DropDown, PickerInteractionMode.Dialog])( + 'should not warn about a wrong overlay id when destroyed while open in %s mode - issue #17938', async (mode) => { + fixture.componentInstance.mode.set(mode); + fixture.detectChanges(); + const warnSpy = vi.spyOn(console, 'warn'); + + fixture.componentInstance.dateRange()!.open(); + await fixture.whenStable(); + expect(fixture.componentInstance.dateRange()!.collapsed).toBe(false); + expect(document.querySelector('igx-calendar-container')).not.toBeNull(); + + fixture.componentInstance.showPicker.set(false); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(fixture.componentInstance.dateRange()).toBeUndefined(); + expect(document.querySelector('igx-calendar-container')).toBeNull(); + expect(warnSpy).not.toHaveBeenCalledWith('igxOverlay.detach was called with wrong id: ', expect.anything()); + }); + }); + describe('Zoneless', () => { beforeEach(async () => { TestBed.resetTestingModule(); @@ -2614,6 +2649,19 @@ export class DateRangeDefaultComponent extends DateRangeTestComponent { public override disabled = false; } +@Component({ + template: ` + @if (showPicker()) { + + }`, + imports: [IgxDateRangePickerComponent] +}) +export class DateRangeConditionalComponent { + public dateRange = viewChild(IgxDateRangePickerComponent); + public showPicker = signal(true); + public mode = signal(PickerInteractionMode.DropDown); +} + @Component({ selector: 'igx-date-range-two-inputs-test', template: ` diff --git a/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.ts b/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.ts index 07ec612e473..ae9d7d5dddf 100644 --- a/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.ts +++ b/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.ts @@ -920,9 +920,12 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective if (this._statusChanges$) { this._statusChanges$.unsubscribe(); } - if (this._overlayId) { + // The overlay service detaches the calendar container on its own when Angular destroys it, + // which happens before this hook when the picker is destroyed with its calendar open. + if (this._overlayId && this._overlayService.getOverlayById(this._overlayId)) { this._overlayService.detach(this._overlayId); } + this._overlayId = ''; } /** @hidden @internal */