From 099da1ab9fa46ef4027c90937026132c917282ca Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Tue, 13 Oct 2026 18:40:13 +0300 Subject: [PATCH 1/6] fix(overlay): hide dynamic content host while detaching it Detaching an overlay moves the host of a dynamically created component back to its original position before destroying the component. Angular may remove the destroyed host later (the animations renderer defers it until the next change detection), so the browser could paint the host outside its hidden wrapper for a frame. With zoneless change detection this made the date and date range picker calendars flash below the input and shift the page layout after a selection. Hide the host of a dynamically created component before unwrapping it. Caller-owned ElementRef content is restored unchanged. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 + .../core/src/services/overlay/overlay.spec.ts | 146 +++++++++++++++++- .../core/src/services/overlay/overlay.ts | 13 ++ .../date-picker/date-picker.component.spec.ts | 34 ++++ 4 files changed, 192 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7d5d1e15d3..51b1629c8cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes for each version of this project will be documented in this - `igx-checkbox`, `igx-switch` and `igx-radio-group` no longer turn invalid while the bound Signal Forms field is untouched and pristine, e.g. right after `reset()` or when a cross-field rule makes a pristine field required. The invalid state now shows only once the field is touched or dirty, as with reactive and template-driven forms. - `IgxAccordionComponent` - When `singleBranchExpand` is set initially and the panels are rendered with `@for`, the panels collapsed on init no longer keep `aria-expanded="true"` and no `ExpressionChangedAfterItHasBeenCheckedError` is thrown in development mode. +- `IgxOverlayService` + - Detaching an overlay that shows a dynamically created component, such as the calendar of the date and date range pickers, no longer renders the component in its original place in the page for a frame before it is removed. This caused the calendar to flash below the input after a date was selected. - `IgxTabsComponent` - The selected tab is now rendered correctly in zoneless applications, including the initially selected tab, selection changes made from code, and selection changes after tabs are added or removed. diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts index 30dfb1eed2c..c441b74693b 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts @@ -1,13 +1,14 @@ import { Component, ComponentRef, ElementRef, HostBinding, inject, Injector, ViewChild, ViewContainerRef, ViewEncapsulation, ChangeDetectionStrategy } from '@angular/core'; import { fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { NoopAnimationsModule, provideAnimations } from '@angular/platform-browser/animations'; import { PlatformUtil } from 'igniteui-angular'; -import { scaleInVerTop, scaleOutVerTop } from 'igniteui-angular/animations'; +import { fadeIn, fadeOut, scaleInVerTop, scaleOutVerTop } from 'igniteui-angular/animations'; import { IgxAvatarComponent } from 'igniteui-angular/avatar'; import { IgxCalendarComponent } from 'igniteui-angular/calendar'; import { IgxCalendarContainerComponent } from 'igniteui-angular/date-picker'; import { IgxToggleDirective } from 'igniteui-angular/directives'; -import { first } from 'rxjs/operators'; +import { firstValueFrom } from 'rxjs'; +import { filter, first } from 'rxjs/operators'; import { UIInteractions, wait } from '../../../../test-utils/ui-interactions.spec'; import { IgxAngularAnimationService } from '../animation/angular-animation-service'; import { IgxOverlayService } from './overlay'; @@ -511,6 +512,40 @@ describe('igxOverlay', () => { overlay.detach(id2); parent.remove(); }); + + it('Should restore a caller-owned element with its own styles on detach', () => { + const parent = document.createElement('div'); + document.body.appendChild(parent); + const element = document.createElement('div'); + element.style.display = 'flex'; + element.style.height = '20px'; + parent.appendChild(element); + const elementRef = new ElementRef(element); + const settings: OverlaySettings = { + modal: false, + positionStrategy: new GlobalPositionStrategy({ openAnimation: null, closeAnimation: null }) + }; + + let id = overlay.attach(elementRef, settings); + overlay.show(id); + overlay.hide(id); + overlay.detach(id); + + expect(element.parentElement).toBe(parent); + expect(element.style.display).toEqual('flex'); + expect(element.getBoundingClientRect().height).toEqual(20); + + // the restored element can be shown again + id = overlay.attach(elementRef, settings); + overlay.show(id); + expect(overlay.getOverlayById(id).visible).toBeTrue(); + expect(element.closest(`.${CLASS_OVERLAY_WRAPPER}`)).not.toBeNull(); + + overlay.detach(id); + expect(element.parentElement).toBe(parent); + expect(element.style.display).toEqual('flex'); + parent.remove(); + }); }); describe('Unit Tests: ', () => { @@ -581,6 +616,34 @@ describe('igxOverlay', () => { overlay.detachAll(); })); + it('Should not expose the host of a destroyed dynamic component on detach', fakeAsync(() => { + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); + const overlay = fixture.componentInstance.overlay; + const variants: { name: string, settings: OverlaySettings }[] = [ + { name: 'in-place', settings: { modal: false } }, + { name: 'outlet', settings: { modal: false, outlet: fixture.componentInstance.divElement } }, + { name: 'container', settings: { modal: false, positionStrategy: new ContainerPositionStrategy() } } + ]; + + for (const { name, settings } of variants) { + const id = overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, settings); + const host: HTMLElement = overlay.getOverlayById(id).componentRef.location.nativeElement; + overlay.show(id); + tick(); + overlay.hide(id); + tick(); + + overlay.detach(id); + // the host may stay in the DOM until Angular removes the destroyed view + expect(host.isConnected && host.checkVisibility()).withContext(name).toBeFalse(); + expect(host.getBoundingClientRect().height).withContext(name).toEqual(0); + + fixture.detectChanges(); + expect(host.isConnected).withContext(name).toBeFalse(); + } + })); + it('Should show component passed to overlay.', fakeAsync(() => { const fixture = TestBed.createComponent(EmptyPageComponent); fixture.detectChanges(); @@ -4832,6 +4895,83 @@ describe('igxOverlay', () => { overlay.detachAll(); })); }); + + describe('Integration tests - dynamic component teardown with animations: ', () => { + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + providers: [provideAnimations()] + }).compileComponents(); + })); + + it('Should not render a dynamic component host outside its wrapper while detaching it', async () => { + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); + const overlay = fixture.componentInstance.overlay; + const hostParent = (fixture.nativeElement as HTMLElement).parentElement; + const initialChildCount = hostParent.childElementCount; + + const id = overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, { + modal: false, + closeOnOutsideClick: false, + positionStrategy: new GlobalPositionStrategy({ openAnimation: fadeIn, closeAnimation: fadeOut }) + }); + const componentRef = overlay.getOverlayById(id).componentRef; + const host: HTMLElement = componentRef.location.nativeElement; + const destroySpy = spyOn(componentRef, 'destroy').and.callThrough(); + + const opened = firstValueFrom(overlay.opened.pipe(filter(e => e.id === id))); + overlay.show(id); + await opened; + expect(host.closest(`.${CLASS_OVERLAY_WRAPPER}`)).not.toBeNull(); + + // once out of its wrapper, the host must neither render nor take part in the page layout + const exposedStates: string[] = []; + const recordState = (moment: string) => { + if (host.isConnected && !host.closest(`.${CLASS_OVERLAY_WRAPPER}`)) { + const { width, height } = host.getBoundingClientRect(); + if (host.checkVisibility({ opacityProperty: true, visibilityProperty: true }) || width || height) { + exposedStates.push(`${moment}: rendered at ${width}x${height}`); + } + } + }; + + let closedCount = 0; + const detached = new Promise(resolve => { + overlay.closed.pipe(filter(e => e.id === id)).subscribe(() => { + closedCount++; + // pickers detach their dynamic content as soon as it is closed + overlay.detach(id); + recordState('after detach'); + resolve(); + }); + }); + let sampling = true; + const sampleFrame = () => { + recordState('animation frame'); + if (sampling) { + requestAnimationFrame(sampleFrame); + } + }; + requestAnimationFrame(sampleFrame); + + overlay.hide(id); + await detached; + for (let frame = 0; frame < 3; frame++) { + await new Promise(resolve => requestAnimationFrame(resolve)); + } + sampling = false; + fixture.detectChanges(); + + expect(exposedStates).toEqual([]); + expect(closedCount).toEqual(1); + expect(destroySpy).toHaveBeenCalledTimes(1); + expect(componentRef.hostView.destroyed).toBeTrue(); + expect(host.isConnected).toBeFalse(); + expect(overlay.getOverlayById(id)).toBeUndefined(); + expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(0); + expect(hostParent.childElementCount).toEqual(initialChildCount); + }); + }); }); @Component({ diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.ts index 0c982bdeb5d..70de53c8b2a 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.ts @@ -790,6 +790,7 @@ export class IgxOverlayService implements OnDestroy { } private cleanUp(info: OverlayInfo) { + this.hideDynamicComponent(info); this.removeWrapper(info); this.restoreHook(info); this.destroyComponent(info); @@ -797,6 +798,18 @@ export class IgxOverlayService implements OnDestroy { this.releaseResources(info); } + /** + * Hides the host of a dynamically created component before it leaves the hidden wrapper. The component + * is destroyed right after, but Angular may remove its host from the DOM later (e.g. with animations enabled), + * so a restored host would otherwise render and take part in the page layout until then. + * Caller-owned elements (`ElementRef`) are restored as they are. + */ + private hideDynamicComponent(info: OverlayInfo) { + if (info.componentRef) { + info.elementRef!.nativeElement.style.display = 'none'; + } + } + /** * Reverses the wrapper insertion performed by `insertWrapper`, restoring the element to its original DOM position. */ 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 766d7132be3..5e51a7c894f 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 @@ -771,6 +771,40 @@ describe('IgxDatePicker', () => { .withContext('focus should return to the picker input') .toBeTrue(); })); + + it('should not render the calendar in the page layout while the closed dropdown is torn down', fakeAsync(() => { + fixture.componentInstance.date = new Date(2026, 8, 15); + fixture.detectChanges(); + datePicker.open(); + tick(); + fixture.detectChanges(); + + const calendarContainer: HTMLElement = fixture.debugElement + .query(By.directive(IgxCalendarContainerComponent)).nativeElement; + const teardownStates = []; + // subscribed after the picker, so it runs right after the picker detaches the closed overlay + const subscription = TestBed.inject(IgxOverlayService).closed.subscribe(() => { + const { width, height } = calendarContainer.getBoundingClientRect(); + teardownStates.push({ + wrapped: !!calendarContainer.closest('.igx-overlay__wrapper'), + rendered: calendarContainer.checkVisibility({ opacityProperty: true, visibilityProperty: true }), + width, + height + }); + }); + + const selectedDate = new Date(2026, 8, 20); + fixture.debugElement.query(By.css(`#${CSS.escape(selectedDate.getTime().toString())}`)).nativeElement.click(); + tick(); + subscription.unsubscribe(); + + expect(datePicker.value).toEqual(selectedDate); + expect(datePicker.collapsed).toBeTrue(); + expect(teardownStates).toEqual([{ wrapped: false, rendered: false, width: 0, height: 0 }]); + + fixture.detectChanges(); + expect(calendarContainer.isConnected).toBeFalse(); + })); }); describe('Input and Display formats', () => { From f46efe31b4f49341911c5151eb3e1528aaeb40fa Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Mon, 12 Oct 2026 14:48:50 +0300 Subject: [PATCH 2/6] fix(date-picker): do not focus input on default toggle click The click on the default calendar icon bubbled to the input group, which focused the input while the calendar opened. The date-time editor then switched the value to the input format and selected it until the calendar received focus, e.g. 9/15/2026 -> 09/15/2026 -> 9/15/2026. With animations disabled, the input even kept the focus instead of the calendar. Stop the click propagation like the projected igx-picker-toggle does and document how to show the same padded value while editing and displaying. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 + .../date-picker/src/date-picker/README.md | 8 +++ .../date-picker/date-picker.component.html | 2 +- .../date-picker/date-picker.component.spec.ts | 59 +++++++++++++++++++ .../src/date-picker/date-picker.component.ts | 7 +++ 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51b1629c8cf..46124837a8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ All notable changes for each version of this project will be documented in this - `igx-checkbox`, `igx-switch` and `igx-radio-group` no longer turn invalid while the bound Signal Forms field is untouched and pristine, e.g. right after `reset()` or when a cross-field rule makes a pristine field required. The invalid state now shows only once the field is touched or dirty, as with reactive and template-driven forms. - `IgxAccordionComponent` - When `singleBranchExpand` is set initially and the panels are rendered with `@for`, the panels collapsed on init no longer keep `aria-expanded="true"` and no `ExpressionChangedAfterItHasBeenCheckedError` is thrown in development mode. +- `IgxDatePickerComponent` + - Clicking the default calendar icon no longer focuses the input while the calendar opens, so its value no longer briefly switches to the `inputFormat` (e.g. `09/15/2026` instead of `9/15/2026`) and gets selected. As with a projected `igx-picker-toggle`, the click on the icon no longer propagates to the picker's ancestors. - `IgxOverlayService` - Detaching an overlay that shows a dynamically created component, such as the calendar of the date and date range pickers, no longer renders the component in its original place in the page for a frame before it is removed. This caused the calendar to flash below the input after a date was selected. - `IgxTabsComponent` diff --git a/projects/igniteui-angular/date-picker/src/date-picker/README.md b/projects/igniteui-angular/date-picker/src/date-picker/README.md index 3034888612d..a849fc5d195 100644 --- a/projects/igniteui-angular/date-picker/src/date-picker/README.md +++ b/projects/igniteui-angular/date-picker/src/date-picker/README.md @@ -53,6 +53,14 @@ Custom formats for the input field. ``` If the `inputFormat` is not set, it will default to the format used by the browser. The `displayFormat` accepts all supported formats by Angular's `DatePipe`. +While the input is focused, the date is edited in the `inputFormat`, which by default uses two-digit day and month parts (e.g. `09/05/2026` for `en-US`). Otherwise the value is shown in the `displayFormat`, which by default uses the locale's numeric format and may omit leading zeros (e.g. `9/5/2026`). To show the same padded value in both states, set the same numeric format to both: +```html + + +``` + +Clicking the default calendar icon opens the calendar and moves the focus to it without focusing the input. Once a date is selected or the calendar is closed with Escape, the focus returns to the input. + DatePicker with cancel and today buttons ```html diff --git a/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.html b/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.html index d011651e47c..e561998c868 100644 --- a/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.html +++ b/projects/igniteui-angular/date-picker/src/date-picker/date-picker.component.html @@ -1,6 +1,6 @@ @if (!toggleComponents.length) { - + { }); }); + describe('With animations', () => { + let fixture: ComponentFixture; + let datePicker: IgxDatePickerComponent; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [IgxDatePickerTestComponent], + providers: [provideAnimations()] + }).compileComponents(); + + fixture = TestBed.createComponent(IgxDatePickerTestComponent); + fixture.componentInstance.date = new Date(2026, 8, 15); + fixture.detectChanges(); + datePicker = fixture.componentInstance.datePicker; + datePicker.locale = 'en-US'; + fixture.detectChanges(); + }); + + it('should move the focus from the input to the calendar as soon as the toggle icon opens it', () => { + const input: HTMLInputElement = fixture.debugElement.query(By.css('input')).nativeElement; + input.focus(); + expect(input.value).toEqual('09/15/2026'); + spyOn(datePicker.opened, 'emit').and.callThrough(); + + const toggleIcon: HTMLElement = fixture.debugElement.query(By.directive(IgxPrefixDirective)) + .query(By.css('igx-icon')).nativeElement; + toggleIcon.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, cancelable: true })); + toggleIcon.click(); + + // the opening animation is still running + expect(datePicker.opened.emit).not.toHaveBeenCalled(); + const calendarWrapper = fixture.debugElement.query(By.css('.igx-calendar__wrapper')).nativeElement; + expect(calendarWrapper.contains(document.activeElement)) + .withContext('focus should move to calendar for KB nav') + .toBeTrue(); + expect(input.value).toEqual('9/15/2026'); + }); + }); + describe('Zoneless', () => { let fixture: ComponentFixture; let datePicker: IgxDatePickerComponent; 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 9baba87d97a..f02239a846d 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 @@ -585,6 +585,10 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr this._overlayId = this._overlayService .attach(IgxCalendarContainerComponent, this.viewContainerRef, overlaySettings); this._overlayService.show(this._overlayId); + if (!this.collapsed) { + // focus the calendar while it opens, so a focused input does not stay in edit mode until then + this._calendar?.wrapper?.nativeElement?.focus(); + } } /** @@ -904,8 +908,6 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr this._overlayService.opened.pipe(...this._overlaySubFilter).subscribe(() => { this.opened.emit({ owner: this }); - - this._calendar.wrapper?.nativeElement?.focus(); }); this._overlayService.closing.pipe(...this._overlaySubFilter).subscribe((e: OverlayEventArgs | OverlayCancelableEventArgs) => { From b32e27bd851f161ec528dd76e0aade726c8a0ed9 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Tue, 13 Oct 2026 18:40:18 +0300 Subject: [PATCH 5/6] docs(overlay): describe the teardown fix by its verified effect The hidden state was confirmed in the DOM and layout during teardown, but a painted frame was not, so do not describe it as a visible flash. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cbc2b7f25fb..1b42dee699e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ All notable changes for each version of this project will be documented in this - Clicking the default calendar icon no longer focuses the input while the calendar opens, so its value no longer briefly switches to the `inputFormat` (e.g. `09/15/2026` instead of `9/15/2026`) and gets selected. As with a projected `igx-picker-toggle`, the click on the icon 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, so an input that was focused no longer stays selected in the `inputFormat` while the calendar opens. - `IgxOverlayService` - - Detaching an overlay that shows a dynamically created component, such as the calendar of the date and date range pickers, no longer renders the component in its original place in the page for a frame before it is removed. This caused the calendar to flash below the input after a date was selected. + - Detaching an overlay that shows a dynamically created component, such as the calendar of the date and date range pickers, now hides the component before moving it back to its original place, so it is no longer left visible there and in the page layout until Angular removes it. - `IgxTabsComponent` - The selected tab is now rendered correctly in zoneless applications, including the initially selected tab, selection changes made from code, and selection changes after tabs are added or removed. From 4b917651afeda01c3d9747725968797388f17c6c Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Tue, 13 Oct 2026 18:40:21 +0300 Subject: [PATCH 6/6] docs(*): shorten comments and docs of the date picker fixes Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++--- .../core/src/services/overlay/overlay.spec.ts | 2 +- .../core/src/services/overlay/overlay.ts | 5 +---- .../date-picker/src/date-picker/README.md | 11 ++--------- .../src/date-picker/date-picker.component.spec.ts | 2 +- .../src/date-picker/date-picker.component.ts | 4 ++-- 6 files changed, 10 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b42dee699e..76f02a7174c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,10 @@ All notable changes for each version of this project will be documented in this - `IgxAccordionComponent` - When `singleBranchExpand` is set initially and the panels are rendered with `@for`, the panels collapsed on init no longer keep `aria-expanded="true"` and no `ExpressionChangedAfterItHasBeenCheckedError` is thrown in development mode. - `IgxDatePickerComponent` - - Clicking the default calendar icon no longer focuses the input while the calendar opens, so its value no longer briefly switches to the `inputFormat` (e.g. `09/15/2026` instead of `9/15/2026`) and gets selected. As with a projected `igx-picker-toggle`, the click on the icon 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, so an input that was focused no longer stays selected in the `inputFormat` while the calendar opens. + - 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. - `IgxOverlayService` - - Detaching an overlay that shows a dynamically created component, such as the calendar of the date and date range pickers, now hides the component before moving it back to its original place, so it is no longer left visible there and in the page layout until Angular removes it. + - Detaching an overlay with a dynamically created component, such as the date picker calendar, no longer leaves the component visible in its original place until Angular removes it. - `IgxTabsComponent` - The selected tab is now rendered correctly in zoneless applications, including the initially selected tab, selection changes made from code, and selection changes after tabs are added or removed. diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts index c441b74693b..d7e0f1d037c 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts @@ -4924,7 +4924,7 @@ describe('igxOverlay', () => { await opened; expect(host.closest(`.${CLASS_OVERLAY_WRAPPER}`)).not.toBeNull(); - // once out of its wrapper, the host must neither render nor take part in the page layout + // once unwrapped, the host must neither render nor take layout space const exposedStates: string[] = []; const recordState = (moment: string) => { if (host.isConnected && !host.closest(`.${CLASS_OVERLAY_WRAPPER}`)) { diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.ts index 70de53c8b2a..01ce55e3acc 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.ts @@ -799,10 +799,7 @@ export class IgxOverlayService implements OnDestroy { } /** - * Hides the host of a dynamically created component before it leaves the hidden wrapper. The component - * is destroyed right after, but Angular may remove its host from the DOM later (e.g. with animations enabled), - * so a restored host would otherwise render and take part in the page layout until then. - * Caller-owned elements (`ElementRef`) are restored as they are. + * Hides a dynamically created component before unwrapping it, as Angular may remove its host after `destroyComponent`. */ private hideDynamicComponent(info: OverlayInfo) { if (info.componentRef) { diff --git a/projects/igniteui-angular/date-picker/src/date-picker/README.md b/projects/igniteui-angular/date-picker/src/date-picker/README.md index 27706365c8b..07072ffefa7 100644 --- a/projects/igniteui-angular/date-picker/src/date-picker/README.md +++ b/projects/igniteui-angular/date-picker/src/date-picker/README.md @@ -53,15 +53,9 @@ Custom formats for the input field. ``` If the `inputFormat` is not set, it will default to the format used by the browser. The `displayFormat` accepts all supported formats by Angular's `DatePipe`. -While the input is focused, the date is edited in the `inputFormat`, which by default uses two-digit day and month parts (e.g. `09/05/2026` for `en-US`). Otherwise the value is shown in the `displayFormat`, which by default uses the locale's numeric format and may omit leading zeros (e.g. `9/5/2026`). To show the same padded value in both states, set the same numeric format to both: -```html - - -``` - -Clicking the default calendar icon opens the calendar and moves the focus to it without focusing the input. Once a date is selected or the calendar is closed with Escape, the focus returns to the input. +While editing, the day and month always have two digits (e.g. `09/05/2026`), whereas the default `displayFormat` may omit leading zeros (e.g. `9/5/2026`). To show the same value in both states, set a padded format such as `MM/dd/yyyy` to both `inputFormat` and `displayFormat`. -To use a custom icon for toggling the calendar, project it in an `igx-picker-toggle`. It replaces the default icon and does not focus the input either: +To use a custom toggle icon, project it in an `igx-picker-toggle`. Unlike other `igx-prefix`/`igx-suffix` content, clicking it does not focus the input: ```html @@ -69,7 +63,6 @@ To use a custom icon for toggling the calendar, project it in an `igx-picker-tog ``` -Clicking other projected `igx-prefix` or `igx-suffix` content focuses the input. A click handler that opens the calendar from such content should stop the event propagation, so the input does not receive focus and switch to the `inputFormat` while the calendar opens. DatePicker with cancel and today buttons 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 959b1e97787..d55447105a1 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 @@ -841,7 +841,7 @@ describe('IgxDatePicker', () => { const calendarContainer: HTMLElement = fixture.debugElement .query(By.directive(IgxCalendarContainerComponent)).nativeElement; const teardownStates = []; - // subscribed after the picker, so it runs right after the picker detaches the closed overlay + // runs right after the picker detaches the closed overlay const subscription = TestBed.inject(IgxOverlayService).closed.subscribe(() => { const { width, height } = calendarContainer.getBoundingClientRect(); teardownStates.push({ 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 f02239a846d..2550d51c9f8 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 @@ -586,7 +586,7 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr .attach(IgxCalendarContainerComponent, this.viewContainerRef, overlaySettings); this._overlayService.show(this._overlayId); if (!this.collapsed) { - // focus the calendar while it opens, so a focused input does not stay in edit mode until then + // do not leave a focused input in edit mode during the opening animation this._calendar?.wrapper?.nativeElement?.focus(); } } @@ -815,7 +815,7 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr /** @hidden @internal */ public onToggleClick(event: MouseEvent): void { - // do not focus the input on click, the calendar receives focus once opened + // do not focus input on click event.stopPropagation(); this.toggle(); }