Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -1750,6 +1751,40 @@ describe('IgxDatePicker', () => {
});
});

describe('Destroy', () => {
let fixture: ComponentFixture<IgxDatePickerConditionalComponent>;

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<IgxDatePickerNgModelComponent>;
let datePicker: IgxDatePickerComponent;
Expand Down Expand Up @@ -1857,6 +1892,19 @@ export class IgxDatePickerTestComponent {
public maxValue: string | Date;
}

@Component({
template: `
@if (showPicker()) {
<igx-date-picker [mode]="mode()"></igx-date-picker>
}`,
imports: [IgxDatePickerComponent]
})
export class IgxDatePickerConditionalComponent {
public datePicker = viewChild(IgxDatePickerComponent);
public showPicker = signal(true);
public mode = signal<PickerInteractionMode>(PickerInteractionMode.DropDown);
}

@Component({
template: `
<igx-date-picker [(ngModel)]="date" [mode]="mode" [minValue]="minValue" [maxValue]="maxValue" [required]="isRequired">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -2405,6 +2405,41 @@ describe('IgxDateRangePicker', () => {
});
});

describe('Destroy', () => {
let fixture: ComponentFixture<DateRangeConditionalComponent>;

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();
Expand Down Expand Up @@ -2614,6 +2649,19 @@ export class DateRangeDefaultComponent extends DateRangeTestComponent {
public override disabled = false;
}

@Component({
template: `
@if (showPicker()) {
<igx-date-range-picker [mode]="mode()"></igx-date-range-picker>
}`,
imports: [IgxDateRangePickerComponent]
})
export class DateRangeConditionalComponent {
public dateRange = viewChild(IgxDateRangePickerComponent);
public showPicker = signal(true);
public mode = signal<PickerInteractionMode>(PickerInteractionMode.DropDown);
}

@Component({
selector: 'igx-date-range-two-inputs-test',
template: `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Loading