From 66c36b1fc5db8540c5f045ce99c02ce4bf6a4817 Mon Sep 17 00:00:00 2001 From: wnvko Date: Thu, 12 Mar 2026 13:02:05 +0200 Subject: [PATCH 1/3] feat(overlay): do not move overlay element --- CHANGELOG.md | 8 + .../calendar-multi-view.component.spec.ts | 8 +- .../core/src/services/overlay/README.md | 4 +- .../core/src/services/overlay/overlay.spec.ts | 1450 ++++++++++------- .../core/src/services/overlay/overlay.ts | 75 +- .../src/services/overlay/position/README.md | 8 +- .../core/src/services/overlay/utilities.ts | 9 +- .../date-picker/src/date-picker/README.md | 2 +- .../calendar-container.component.ts | 1 + .../src/date-picker/date-picker.component.ts | 4 + .../src/date-picker/picker-base.directive.ts | 4 + .../src/date-range-picker/README.md | 2 +- .../date-range-picker.component.spec.ts | 2 +- .../date-range-picker.component.ts | 11 +- .../src/dialog/dialog.component.spec.ts | 7 +- .../notification/notifications.directive.ts | 4 + .../src/directives/toggle/README.md | 6 +- .../src/directives/toggle/toggle.directive.ts | 4 + .../src/drop-down/autocomplete/README.md | 4 +- .../autocomplete/autocomplete.directive.ts | 7 +- .../grids/core/src/common/grid.interface.ts | 5 + ...xcel-style-default-expression.component.ts | 5 + .../grids/grid/src/grid-base.directive.ts | 6 + .../src/hierarchical-grid-base.directive.ts | 8 + .../pivot-grid/src/pivot-grid.component.ts | 1 + .../query-builder-tree.component.ts | 3 + .../query-builder/query-builder.component.ts | 1 + .../test-utils/ui-interactions.spec.ts | 4 +- src/app/overlay/overlay.sample.ts | 2 +- 29 files changed, 984 insertions(+), 671 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db4196e3639..804e1c0aad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes for each version of this project will be documented in this file. +## 21.1.1 + +### New Features + +- `IgxOverlayService` + - The overlay service now integrates the HTML Popover API and prefers rendering content in place / in the top layer, significantly reducing the need for outlet containers. When configured, `OverlaySettings.outlet` is still honored, and overlays may also fall back to being appended to `document.body` when required. + + - **Deprecation** - The `outlet` property in `OverlaySettings`, `IgxOverlayOutletDirective`, and `igxToggleOutlet` input on `IgxToggleActionDirective` are deprecated and will be removed in a future version. They remain functional in this release for backward compatibility, but new code should rely on the default in-place / top-layer rendering behavior instead of custom outlet containers. ## 21.1.0 ### New Features diff --git a/projects/igniteui-angular/calendar/src/calendar/calendar-multi-view.component.spec.ts b/projects/igniteui-angular/calendar/src/calendar/calendar-multi-view.component.spec.ts index c62c2dd72a3..19e9b044c67 100644 --- a/projects/igniteui-angular/calendar/src/calendar/calendar-multi-view.component.spec.ts +++ b/projects/igniteui-angular/calendar/src/calendar/calendar-multi-view.component.spec.ts @@ -1045,7 +1045,7 @@ describe('Multi-View Calendar - ', () => { tick(400); fixture.detectChanges(); - let overlay = document.querySelector(HelperTestFunctions.OVERLAY_CSSCLASS); + let overlay = document.querySelector(HelperTestFunctions.CALENDAR_WRAPPER_CLASS); HelperTestFunctions.verifyMonthsViewNumber(overlay, 3); HelperTestFunctions.verifyCalendarSubHeaders(overlay, [ymd('2019-09-16'), ymd('2019-10-16'), ymd('2019-11-16')]); @@ -1064,7 +1064,7 @@ describe('Multi-View Calendar - ', () => { tick(400); fixture.detectChanges(); - overlay = document.querySelector(HelperTestFunctions.OVERLAY_CSSCLASS); + overlay = document.querySelector(HelperTestFunctions.CALENDAR_WRAPPER_CLASS); HelperTestFunctions.verifyMonthsViewNumber(overlay, 2); HelperTestFunctions.verifyCalendarSubHeaders(overlay, [ymd('2019-09-16'), ymd('2019-10-16')]); @@ -1079,7 +1079,7 @@ describe('Multi-View Calendar - ', () => { fixture.detectChanges(); expect(datePicker.hideOutsideDays).toBe(true); - let overlay = document.querySelector(HelperTestFunctions.OVERLAY_CSSCLASS); + let overlay = document.querySelector(HelperTestFunctions.CALENDAR_WRAPPER_CLASS); expect(HelperTestFunctions.getHiddenDays(overlay, 0).length).toBe(HelperTestFunctions.getInactiveDays(overlay, 0).length); expect(HelperTestFunctions.getHiddenDays(overlay, 1).length).toBe(HelperTestFunctions.getInactiveDays(overlay, 1).length); expect(HelperTestFunctions.getHiddenDays(overlay, 2).length).toBe(HelperTestFunctions.getInactiveDays(overlay, 2).length); @@ -1098,7 +1098,7 @@ describe('Multi-View Calendar - ', () => { fixture.detectChanges(); expect(datePicker.hideOutsideDays).toBe(false); - overlay = document.querySelector(HelperTestFunctions.OVERLAY_CSSCLASS); + overlay = document.querySelector(HelperTestFunctions.CALENDAR_WRAPPER_CLASS); expect(HelperTestFunctions.getHiddenDays(overlay, 0).length).toBe(12); expect(HelperTestFunctions.getHiddenDays(overlay, 1).length).toBe(11); expect(HelperTestFunctions.getHiddenDays(overlay, 2).length).toBe(5); diff --git a/projects/igniteui-angular/core/src/services/overlay/README.md b/projects/igniteui-angular/core/src/services/overlay/README.md index f6b2ce1e8bc..d47423fd8db 100644 --- a/projects/igniteui-angular/core/src/services/overlay/README.md +++ b/projects/igniteui-angular/core/src/services/overlay/README.md @@ -78,7 +78,7 @@ this.overlay.show(component, overlaySettings); | scrollStrategy | IScrollStrategy | Scroll strategy to use with this settings | | modal | boolean | Set if the overlay should be in modal mode | | closeOnOutsideClick | boolean | Set if the overlay should closed on outside click | -| outlet | IgxOverlayOutletDirective or ElementRef | Set the outlet container to attach the overlay to | +| outlet | IgxOverlayOutletDirective or ElementRef | **Deprecated.** Still supported and used by the overlay service when provided but will be removed in a future version. Avoid using this property in new code and prefer the default in-place rendering with the HTML Popover API. | ###### PositionSettings @@ -135,7 +135,7 @@ this.overlay.show(component, overlaySettings); | Name | Description | Parameters | |-----------------|---------------------------------------------------------------------------------|------------| |getPointFromPositionsSettings| Calculates the point from which the overlay should start showing |settings | -|createAbsoluteOverlaySettings| Creates overlay settings with global or container position strategy based on a preset position settings |position?, outlet?| +|createAbsoluteOverlaySettings| Creates overlay settings with a global or container position strategy based on preset position settings and the provided outlet. When an outlet is specified, a container strategy is used and the content is rendered inside that outlet; otherwise, a global strategy is used and the content is rendered in place. |position?, outlet?| |createRelativeOverlaySettings| Creates overlay settings with auto, connected or elastic position strategy based on a preset position settings |target, strategy?, position?| 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 f5130815fd6..0685394a36f 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts @@ -31,6 +31,7 @@ import { OverlaySettings, Point, PositionSettings, + Util, VerticalAlignment } from './utilities'; @@ -40,7 +41,6 @@ const CLASS_OVERLAY_CONTENT_RELATIVE = 'igx-overlay__content--relative'; const CLASS_OVERLAY_WRAPPER = 'igx-overlay__wrapper'; const CLASS_OVERLAY_WRAPPER_MODAL = 'igx-overlay__wrapper--modal'; const CLASS_OVERLAY_WRAPPER_FLEX = 'igx-overlay__wrapper--flex'; -const CLASS_OVERLAY_MAIN = 'igx-overlay'; const CLASS_SCROLLABLE_DIV = 'scrollableDiv'; const DEBOUNCE_TIME = 16; @@ -88,80 +88,80 @@ export const addScrollDivToElement = (parent) => { * @param screenRect Rectangle of the visible area * @param elastic Is elastic position strategy, defaults to false */ -// const getOverlayWrapperLocation = ( -// positionSettings: PositionSettings, -// targetRect: ClientRect, -// wrapperRect: ClientRect, -// screenRect: ClientRect, -// elastic = false): Point => { -// const location: Point = new Point(0, 0); - -// location.x = -// targetRect.left + -// targetRect.width * (1 + positionSettings.horizontalStartPoint) + -// wrapperRect.width * positionSettings.horizontalDirection; -// if (location.x < screenRect.left) { -// if (elastic) { -// let offset = screenRect.left - location.x; -// if (offset > wrapperRect.width - positionSettings.minSize.width) { -// offset = wrapperRect.width - positionSettings.minSize.width; -// } -// location.x += offset; -// } else { -// const flipOffset = wrapperRect.width * (1 + positionSettings.horizontalDirection); -// if (positionSettings.horizontalStartPoint === HorizontalAlignment.Left) { -// location.x = Math.max(0, targetRect.right - flipOffset); -// } else if (positionSettings.horizontalStartPoint === HorizontalAlignment.Center) { -// location.x = -// Math.max(0, targetRect.left + targetRect.width / 2 - flipOffset); -// } else { -// location.x = Math.max(0, targetRect.left - flipOffset); -// } -// } -// } else if (location.x + wrapperRect.width > screenRect.right && !elastic) { -// const flipOffset = wrapperRect.width * (1 + positionSettings.horizontalDirection); -// if (positionSettings.horizontalStartPoint === HorizontalAlignment.Left) { -// location.x = Math.min(screenRect.right, targetRect.right - flipOffset); -// } else if (positionSettings.horizontalStartPoint === HorizontalAlignment.Center) { -// location.x = Math.min(screenRect.right, targetRect.left + targetRect.width / 2 - flipOffset); -// } else { -// location.x = Math.min(screenRect.right, targetRect.left - flipOffset); -// } -// } - -// location.y = -// targetRect.top + -// targetRect.height * (1 + positionSettings.verticalStartPoint) + -// wrapperRect.height * positionSettings.verticalDirection; -// if (location.y < screenRect.top) { -// if (elastic) { -// let offset = screenRect.top - location.y; -// if (offset > wrapperRect.height - positionSettings.minSize.height) { -// offset = wrapperRect.height - positionSettings.minSize.height; -// } -// location.y += offset; -// } else { -// const flipOffset = wrapperRect.height * (1 + positionSettings.verticalDirection); -// if (positionSettings.verticalStartPoint === VerticalAlignment.Top) { -// location.y = Math.max(0, targetRect.bottom - flipOffset); -// } else if (positionSettings.verticalStartPoint === VerticalAlignment.Middle) { -// location.y = Math.max(0, targetRect.top + targetRect.height / 2 - flipOffset); -// } else { -// location.y = Math.max(0, targetRect.top - flipOffset); -// } -// } -// } else if (location.y + wrapperRect.height > screenRect.bottom && !elastic) { -// const flipOffset = wrapperRect.height * (1 + positionSettings.verticalDirection); -// if (positionSettings.verticalStartPoint === VerticalAlignment.Top) { -// location.y = Math.min(screenRect.bottom, targetRect.bottom - flipOffset); -// } else if (positionSettings.verticalStartPoint === VerticalAlignment.Middle) { -// location.y = Math.min(screenRect.bottom, targetRect.top + targetRect.height / 2 - flipOffset); -// } else { -// location.y = Math.min(screenRect.bottom, targetRect.top - flipOffset); -// } -// } -// return location; -// }; +const getOverlayWrapperLocation = ( + positionSettings: PositionSettings, + targetRect: DOMRect, + wrapperRect: DOMRect, + screenRect: DOMRect, + elastic = false): Point => { + const location: Point = new Point(0, 0); + + location.x = + targetRect.left + + targetRect.width * (1 + positionSettings.horizontalStartPoint) + + wrapperRect.width * positionSettings.horizontalDirection; + if (location.x < screenRect.left) { + if (elastic) { + let offset = screenRect.left - location.x; + if (offset > wrapperRect.width - positionSettings.minSize.width) { + offset = wrapperRect.width - positionSettings.minSize.width; + } + location.x += offset; + } else { + const flipOffset = wrapperRect.width * (1 + positionSettings.horizontalDirection); + if (positionSettings.horizontalStartPoint === HorizontalAlignment.Left) { + location.x = Math.max(0, targetRect.right - flipOffset); + } else if (positionSettings.horizontalStartPoint === HorizontalAlignment.Center) { + location.x = + Math.max(0, targetRect.left + targetRect.width / 2 - flipOffset); + } else { + location.x = Math.max(0, targetRect.left - flipOffset); + } + } + } else if (location.x + wrapperRect.width > screenRect.right && !elastic) { + const flipOffset = wrapperRect.width * (1 + positionSettings.horizontalDirection); + if (positionSettings.horizontalStartPoint === HorizontalAlignment.Left) { + location.x = Math.min(screenRect.right, targetRect.right - flipOffset); + } else if (positionSettings.horizontalStartPoint === HorizontalAlignment.Center) { + location.x = Math.min(screenRect.right, targetRect.left + targetRect.width / 2 - flipOffset); + } else { + location.x = Math.min(screenRect.right, targetRect.left - flipOffset); + } + } + + location.y = + targetRect.top + + targetRect.height * (1 + positionSettings.verticalStartPoint) + + wrapperRect.height * positionSettings.verticalDirection; + if (location.y < screenRect.top) { + if (elastic) { + let offset = screenRect.top - location.y; + if (offset > wrapperRect.height - positionSettings.minSize.height) { + offset = wrapperRect.height - positionSettings.minSize.height; + } + location.y += offset; + } else { + const flipOffset = wrapperRect.height * (1 + positionSettings.verticalDirection); + if (positionSettings.verticalStartPoint === VerticalAlignment.Top) { + location.y = Math.max(0, targetRect.bottom - flipOffset); + } else if (positionSettings.verticalStartPoint === VerticalAlignment.Middle) { + location.y = Math.max(0, targetRect.top + targetRect.height / 2 - flipOffset); + } else { + location.y = Math.max(0, targetRect.top - flipOffset); + } + } + } else if (location.y + wrapperRect.height > screenRect.bottom && !elastic) { + const flipOffset = wrapperRect.height * (1 + positionSettings.verticalDirection); + if (positionSettings.verticalStartPoint === VerticalAlignment.Top) { + location.y = Math.min(screenRect.bottom, targetRect.bottom - flipOffset); + } else if (positionSettings.verticalStartPoint === VerticalAlignment.Middle) { + location.y = Math.min(screenRect.bottom, targetRect.top + targetRect.height / 2 - flipOffset); + } else { + location.y = Math.min(screenRect.bottom, targetRect.top - flipOffset); + } + } + return location; +}; /** * Formats a string according to the given formatters @@ -312,6 +312,205 @@ describe('igxOverlay', () => { listener!(new KeyboardEvent('keydown', { key: 'Escape' })); expect(hideSpy).toHaveBeenCalledTimes(1); }); + + it('Should wrap element in-place', () => { + const parent = document.createElement('div'); + document.body.appendChild(parent); + const element = document.createElement('div'); + element.classList.add('test-element'); + parent.appendChild(element); + const elementRef = new ElementRef(element); + + const settings: OverlaySettings = { + modal: false, + positionStrategy: new GlobalPositionStrategy({ openAnimation: null, closeAnimation: null }) + }; + const id = overlay.attach(elementRef, settings); + + // The element should still be inside its original parent's DOM tree + expect(parent.contains(element)).toBeTrue(); + // A wrapper should have been inserted in the parent + const wrapper = parent.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; + expect(wrapper).toBeDefined(); + // The element should be inside the wrapper's content div + expect(element.parentElement.classList.contains(CLASS_OVERLAY_CONTENT)).toBeTrue(); + + overlay.detach(id); + parent.remove(); + }); + + it('Should move element to outlet when outlet is provided', () => { + const parent = document.createElement('div'); + document.body.appendChild(parent); + const element = document.createElement('div'); + element.classList.add('test-element'); + parent.appendChild(element); + const elementRef = new ElementRef(element); + + const settings: OverlaySettings = { + modal: false, + outlet: new ElementRef(outlet), + positionStrategy: new GlobalPositionStrategy({ openAnimation: null, closeAnimation: null }) + }; + const id = overlay.attach(elementRef, settings); + + // The element should have been moved to the outlet + expect(outlet.contains(element)).toBeTrue(); + // The element should no longer be in the original parent (only the hook remains) + expect(parent.contains(element)).toBeFalse(); + + overlay.detach(id); + parent.remove(); + }); + + it('Should not create a hook element when outlet is not provided', () => { + const parent = document.createElement('div'); + document.body.appendChild(parent); + const element = document.createElement('div'); + parent.appendChild(element); + const elementRef = new ElementRef(element); + + const settings: OverlaySettings = { + modal: false, + positionStrategy: new GlobalPositionStrategy({ openAnimation: null, closeAnimation: null }) + }; + const id = overlay.attach(elementRef, settings); + + // No hidden hook div should exist in the parent + const hookElements = Array.from(parent.querySelectorAll('div[style*="display: none"]')); + expect(hookElements.length).toEqual(0); + + overlay.detach(id); + parent.remove(); + }); + + it('Should unwrap element on detach', () => { + const parent = document.createElement('div'); + document.body.appendChild(parent); + const element = document.createElement('div'); + element.classList.add('test-element'); + parent.appendChild(element); + const elementRef = new ElementRef(element); + + const settings: OverlaySettings = { + modal: false, + positionStrategy: new GlobalPositionStrategy({ openAnimation: null, closeAnimation: null }) + }; + const id = overlay.attach(elementRef, settings); + + // Verify element is wrapped + expect(element.parentElement.classList.contains(CLASS_OVERLAY_CONTENT)).toBeTrue(); + + overlay.detach(id); + + // After detach, element should be back as a direct child of parent + expect(element.parentElement).toBe(parent); + // No wrapper should remain + const wrapper = parent.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; + expect(wrapper).toBeUndefined(); + + parent.remove(); + }); + + it('Should show and hide overlay in-place', () => { + const parent = document.createElement('div'); + document.body.appendChild(parent); + const element = document.createElement('div'); + parent.appendChild(element); + const elementRef = new ElementRef(element); + + const settings: OverlaySettings = { + modal: false, + positionStrategy: new GlobalPositionStrategy({ openAnimation: null, closeAnimation: null }) + }; + const id = overlay.attach(elementRef, settings); + + overlay.show(id); + const info = overlay.getOverlayById(id); + expect(info.visible).toBeTrue(); + // The wrapper should have popover attribute + expect(info.wrapperElement.getAttribute('popover')).toEqual('manual'); + + overlay.hide(id); + expect(info.visible).toBeFalse(); + + overlay.detach(id); + parent.remove(); + }); + + it('Should support multiple in-place overlays simultaneously', () => { + const parent1 = document.createElement('div'); + document.body.appendChild(parent1); + const element1 = document.createElement('div'); + element1.classList.add('element-1'); + parent1.appendChild(element1); + + const parent2 = document.createElement('div'); + document.body.appendChild(parent2); + const element2 = document.createElement('div'); + element2.classList.add('element-2'); + parent2.appendChild(element2); + + const settings: OverlaySettings = { + modal: false, + positionStrategy: new GlobalPositionStrategy({ openAnimation: null, closeAnimation: null }) + }; + + const id1 = overlay.attach(new ElementRef(element1), settings); + const id2 = overlay.attach(new ElementRef(element2), settings); + + // Both elements should be in-place wrapped + expect(parent1.contains(element1)).toBeTrue(); + expect(parent2.contains(element2)).toBeTrue(); + + overlay.show(id1); + overlay.show(id2); + + expect(overlay.getOverlayById(id1).visible).toBeTrue(); + expect(overlay.getOverlayById(id2).visible).toBeTrue(); + + overlay.detach(id1); + overlay.detach(id2); + + // Elements should be unwrapped back to their parents + expect(element1.parentElement).toBe(parent1); + expect(element2.parentElement).toBe(parent2); + + parent1.remove(); + parent2.remove(); + }); + + it('Should allow mixing in-place and outlet overlay modes', () => { + const parent = document.createElement('div'); + document.body.appendChild(parent); + const element1 = document.createElement('div'); + parent.appendChild(element1); + + const element2 = document.createElement('div'); + parent.appendChild(element2); + + const inPlaceSettings: OverlaySettings = { + modal: false, + positionStrategy: new GlobalPositionStrategy({ openAnimation: null, closeAnimation: null }) + }; + const outletSettings: OverlaySettings = { + modal: false, + outlet: new ElementRef(outlet), + positionStrategy: new GlobalPositionStrategy({ openAnimation: null, closeAnimation: null }) + }; + + const id1 = overlay.attach(new ElementRef(element1), inPlaceSettings); + const id2 = overlay.attach(new ElementRef(element2), outletSettings); + + // in-place element stays in parent + expect(parent.contains(element1)).toBeTrue(); + // outlet element is moved to the outlet + expect(outlet.contains(element2)).toBeTrue(); + + overlay.detach(id1); + overlay.detach(id2); + parent.remove(); + }); }); describe('Unit Tests: ', () => { @@ -329,16 +528,16 @@ describe('igxOverlay', () => { }).compileComponents(); })); - it('OverlayElement should return a div attached to Document\'s body.', fakeAsync(() => { + it('should return a div attached to Document\'s body when dynamic component is shown without viewContainerRef', fakeAsync(() => { const fixture = TestBed.createComponent(EmptyPageComponent); fixture.detectChanges(); fixture.componentInstance.buttonElement.nativeElement.click(); - tick(); - const overlayDiv = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayDiv).toBeDefined(); - expect(overlayDiv).toHaveClass(CLASS_OVERLAY_MAIN); + tick(DEBOUNCE_TIME); + const wrapperDiv = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_MODAL)[0] as HTMLElement; + expect(wrapperDiv).toBeDefined(); + expect(wrapperDiv).toHaveClass(CLASS_OVERLAY_WRAPPER_MODAL); fixture.componentInstance.overlay.detachAll(); })); @@ -349,7 +548,7 @@ describe('igxOverlay', () => { const overlay = fixture.componentInstance.overlay; fixture.detectChanges(); - let id = overlay.attach(SimpleDynamicComponent, { outlet: button, modal: false }); + let id = overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, { outlet: button, modal: false }); overlay.show(id); tick(); @@ -360,19 +559,18 @@ describe('igxOverlay', () => { overlay.detach(id); tick(); - id = overlay.attach(SimpleDynamicComponent, { modal: false }); + id = overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, { modal: false }); overlay.show(id); tick(); wrapperElement = (fixture.nativeElement as HTMLElement) .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0] as HTMLElement; expect(wrapperElement).toBeDefined(); - expect(wrapperElement.parentElement.classList).toContain(CLASS_OVERLAY_MAIN); - expect(wrapperElement.parentElement.parentElement).toBe(document.body); + expect((fixture.nativeElement as HTMLElement).parentElement.contains(wrapperElement)).toBeTrue(); overlay.detach(id); tick(); fixture.debugElement.nativeElement.appendChild(outlet); - id = overlay.attach(SimpleDynamicComponent, { modal: false, outlet: TestBed.inject(IgxOverlayOutletDirective) }); + id = overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, { modal: false, outlet: TestBed.inject(IgxOverlayOutletDirective) }); overlay.show(id); tick(); wrapperElement = (fixture.nativeElement as HTMLElement) @@ -390,12 +588,8 @@ describe('igxOverlay', () => { fixture.componentInstance.buttonElement.nativeElement.click(); tick(DEBOUNCE_TIME); - const overlayElement = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayElement).toBeDefined(); - expect(overlayElement.children.length).toEqual(1); - - const wrapperElement = overlayElement.children[0]; + const wrapperElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_MODAL)[0] as HTMLElement; expect(wrapperElement).toBeDefined(); expect(wrapperElement).toHaveClass(CLASS_OVERLAY_WRAPPER_MODAL); expect(wrapperElement.children[0].localName).toEqual('div'); @@ -413,34 +607,31 @@ describe('igxOverlay', () => { const overlay = fixture.componentInstance.overlay; - overlay.show(overlay.attach(SimpleDynamicComponent)); - tick(); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef)); + tick(DEBOUNCE_TIME); - overlay.show(overlay.attach(SimpleDynamicComponent)); - tick(); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef)); + tick(DEBOUNCE_TIME); - let overlayDiv = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayDiv).toBeDefined(); - expect(overlayDiv.children.length).toEqual(2); - expect((overlayDiv.children[0] as HTMLElement).style.visibility).toEqual(''); - expect((overlayDiv.children[1] as HTMLElement).style.visibility).toEqual(''); + const wrappers = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_MODAL); + expect(wrappers.length).toEqual(2); + const wrapper0 = wrappers[0] as HTMLElement; + const wrapper1 = wrappers[1] as HTMLElement; + expect(wrapper0.style.visibility).toEqual(''); + expect(wrapper1.style.visibility).toEqual(''); overlay.hide('0'); tick(); - overlayDiv = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect((overlayDiv.children[0] as HTMLElement).style.visibility).toEqual('hidden'); - expect((overlayDiv.children[1] as HTMLElement).style.visibility).toEqual(''); + expect(wrapper0.style.visibility).toEqual('hidden'); + expect(wrapper1.style.visibility).toEqual(''); overlay.hide('1'); tick(); - overlayDiv = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect((overlayDiv.children[0] as HTMLElement).style.visibility).toEqual('hidden'); - expect((overlayDiv.children[1] as HTMLElement).style.visibility).toEqual('hidden'); + expect(wrapper0.style.visibility).toEqual('hidden'); + expect(wrapper1.style.visibility).toEqual('hidden'); overlay.detachAll(); })); @@ -450,24 +641,23 @@ describe('igxOverlay', () => { fixture.detectChanges(); const overlay = fixture.componentInstance.overlay; - overlay.show(overlay.attach(SimpleDynamicComponent)); - overlay.show(overlay.attach(SimpleDynamicComponent)); - tick(); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef)); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef)); + tick(DEBOUNCE_TIME); fixture.detectChanges(); - let overlayDiv = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayDiv).toBeDefined(); - expect(overlayDiv.children.length).toEqual(2); - expect((overlayDiv.children[0] as HTMLElement).style.visibility).toEqual(''); - expect((overlayDiv.children[1] as HTMLElement).style.visibility).toEqual(''); + const wrappers = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_MODAL); + expect(wrappers.length).toEqual(2); + const wrapper0 = wrappers[0] as HTMLElement; + const wrapper1 = wrappers[1] as HTMLElement; + expect(wrapper0.style.visibility).toEqual(''); + expect(wrapper1.style.visibility).toEqual(''); overlay.hideAll(); tick(); - overlayDiv = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect((overlayDiv.children[0] as HTMLElement).style.visibility).toEqual('hidden'); - expect((overlayDiv.children[1] as HTMLElement).style.visibility).toEqual('hidden'); + expect(wrapper0.style.visibility).toEqual('hidden'); + expect(wrapper1.style.visibility).toEqual('hidden'); overlay.detachAll(); })); @@ -478,17 +668,17 @@ describe('igxOverlay', () => { fixture.componentInstance.show(); tick(); - let overlayDiv = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayDiv).toBeDefined(); - expect(overlayDiv.children.length).toEqual(1); - expect(overlayDiv.children[0].localName).toEqual('div'); + let overlayWrapper = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_FLEX)[0] as HTMLElement; + expect(overlayWrapper).toBeDefined(); + expect(overlayWrapper.children.length).toEqual(1); + expect(overlayWrapper.children[0].localName).toEqual('div'); fixture.componentInstance.hide(); tick(); - overlayDiv = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayDiv).toBeUndefined(); + overlayWrapper = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_FLEX)[0] as HTMLElement; + expect(overlayWrapper).toBeUndefined(); })); it('Should properly emit events.', fakeAsync(() => { @@ -831,14 +1021,14 @@ describe('igxOverlay', () => { button1.click(); tick(); - const overlayDiv = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - const wrapperElement1 = overlayDiv.children[0] as HTMLElement; + const wrappers = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER); + const wrapperElement1 = wrappers[0] as HTMLElement; expect(wrapperElement1.style.visibility).toEqual(''); button2.click(); tick(); - const wrapperElement2 = overlayDiv.children[1] as HTMLElement; + const wrapperElement2 = wrappers[1] as HTMLElement; expect(wrapperElement1.style.visibility).toEqual(''); expect(wrapperElement2.style.visibility).toEqual(''); @@ -851,9 +1041,9 @@ describe('igxOverlay', () => { fixture.componentInstance.show(overlaySettings); tick(); - let overlayElement = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayElement).toBeDefined(); + let overlayWrapperElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_FLEX)[0] as HTMLElement; + expect(overlayWrapperElement).toBeDefined(); const scrollableDiv = (fixture.nativeElement as HTMLElement) .parentElement.getElementsByClassName(CLASS_SCROLLABLE_DIV)[0] as HTMLElement; @@ -861,67 +1051,66 @@ describe('igxOverlay', () => { scrollableDiv.dispatchEvent(new Event('scroll')); tick(); - overlayElement = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayElement).toBeDefined(); + overlayWrapperElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_FLEX)[0] as HTMLElement; + expect(overlayWrapperElement).toBeDefined(); scrollableDiv.scrollTop += 100; scrollableDiv.dispatchEvent(new Event('scroll')); tick(); - overlayElement = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayElement).toBeDefined(); + overlayWrapperElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_FLEX)[0] as HTMLElement; + expect(overlayWrapperElement).toBeDefined(); fixture.componentInstance.hide(); })); - // TODO: refactor utilities to include all exported methods in a class - // it('#1799 - content div should reposition on window resize.', fakeAsync(() => { - // const rect: ClientRect = { - // bottom: 50, - // height: 0, - // left: 50, - // right: 50, - // top: 50, - // width: 0 - // }; - // const getPointSpy = spyOn(Util, 'getTargetRect').and.returnValue(rect); - // const fixture = TestBed.createComponent(FlexContainerComponent); - // fixture.detectChanges(); - // const overlayInstance = fixture.componentInstance.overlay; - // const buttonElement: HTMLElement = fixture.componentInstance.buttonElement.nativeElement; - - // const id = overlayInstance.attach( - // SimpleDynamicComponent, - // { positionStrategy: new ConnectedPositioningStrategy({ target: buttonElement }) }); - // overlayInstance.show(id); - // tick(DEBOUNCE_TIME); - - // let contentElement = (fixture.nativeElement as HTMLElement) - // .parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT_MODAL)[0] as HTMLElement; - // let contentRect = contentElement.getBoundingClientRect(); - - // expect(50).toEqual(contentRect.left); - // expect(50).toEqual(contentRect.top); - - // rect.left = 200; - // rect.right = 200; - // rect.top = 200; - // rect.bottom = 200; - // getPointSpy.and.callThrough().and.returnValue(rect); - // window.resizeBy(200, 200); - // window.dispatchEvent(new Event('resize')); - // tick(DEBOUNCE_TIME); - - // contentElement = (fixture.nativeElement as HTMLElement) - // .parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT_MODAL)[0] as HTMLElement; - // contentRect = contentElement.getBoundingClientRect(); - // expect(200).toEqual(contentRect.left); - // expect(200).toEqual(contentRect.top); - - // overlayInstance.hide(id); - // })); + it('#1799 - content div should reposition on window resize.', fakeAsync(() => { + const rect = { + bottom: 50, + height: 0, + left: 50, + right: 50, + top: 50, + width: 0 + }; + const getPointSpy = spyOn(Util, 'getTargetRect').and.returnValue(rect); + const fixture = TestBed.createComponent(FlexContainerComponent); + fixture.detectChanges(); + const overlayInstance = fixture.componentInstance.overlay; + const buttonElement: HTMLElement = fixture.componentInstance.buttonElement.nativeElement; + + const id = overlayInstance.attach( + SimpleDynamicComponent, + { positionStrategy: new ConnectedPositioningStrategy(), target: buttonElement }); + overlayInstance.show(id); + tick(DEBOUNCE_TIME); + + let contentElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT_MODAL)[0] as HTMLElement; + let contentRect = contentElement.getBoundingClientRect(); + + expect(50).toBeCloseTo(contentRect.left, 0); + expect(50).toBeCloseTo(contentRect.top, 0); + + rect.left = 200; + rect.right = 200; + rect.top = 200; + rect.bottom = 200; + getPointSpy.and.callThrough().and.returnValue(rect); + window.resizeBy(200, 200); + window.dispatchEvent(new Event('resize')); + tick(DEBOUNCE_TIME); + + contentElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT_MODAL)[0] as HTMLElement; + contentRect = contentElement.getBoundingClientRect(); + expect(200).toBeCloseTo(contentRect.left, 0); + expect(200).toBeCloseTo(contentRect.top, 0); + + overlayInstance.hide(id); + })); it('#2475 - An error is thrown for IgxOverlay when showing a component' + 'instance that is not attached to the DOM', fakeAsync(() => { @@ -947,9 +1136,8 @@ describe('igxOverlay', () => { expect(wrapperElement).toHaveClass(CLASS_OVERLAY_WRAPPER_MODAL); expect(wrapperElement).toHaveClass(CLASS_OVERLAY_WRAPPER_FLEX); - const overlayElement = wrapperElement.parentElement; - expect(overlayElement).toBeDefined(); - expect(overlayElement).toHaveClass(CLASS_OVERLAY_MAIN); + // The wrapper is appended directly to document.body since the element was detached from the DOM + expect(wrapperElement.parentElement).toBe(document.body); overlay.detachAll(); })); @@ -1032,28 +1220,25 @@ describe('igxOverlay', () => { closeOnOutsideClick: true }; - overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, overlaySettings)); overlaySettings.positionStrategy.settings.horizontalStartPoint = HorizontalAlignment.Right; - overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, overlaySettings)); fixture.detectChanges(); tick(); - let overlayElement = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayElement).toBeDefined(); - expect(overlayElement.childElementCount).toEqual(2); - expect((overlayElement.children[0] as HTMLElement).style.visibility).toEqual(''); - expect((overlayElement.children[1] as HTMLElement).style.visibility).toEqual(''); + const wrappers = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER); + expect(wrappers.length).toEqual(2); + expect((wrappers[0] as HTMLElement).style.visibility).toEqual(''); + expect((wrappers[1] as HTMLElement).style.visibility).toEqual(''); (overlay as any)._overlayInfos[0].elementRef.nativeElement.click(); fixture.detectChanges(); tick(); - overlayElement = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayElement.childElementCount).toEqual(2); - expect((overlayElement.children[0] as HTMLElement).style.visibility).toEqual(''); - expect((overlayElement.children[1] as HTMLElement).style.visibility).toEqual('hidden'); + expect(wrappers.length).toEqual(2); + expect((wrappers[0] as HTMLElement).style.visibility).toEqual(''); + expect((wrappers[1] as HTMLElement).style.visibility).toEqual('hidden'); overlay.detachAll(); })); @@ -1200,45 +1385,45 @@ describe('igxOverlay', () => { overlay.detachAll(); }); - // it('##6474 - should calculate correctly position', () => { - // const elastic: ElasticPositionStrategy = new ElasticPositionStrategy(); - // const targetRect: ClientRect = { - // top: 100, - // bottom: 200, - // height: 100, - // left: 100, - // right: 200, - // width: 100 - // }; - // const elementRect: ClientRect = { - // top: 0, - // bottom: 300, - // height: 300, - // left: 0, - // right: 300, - // width: 300 - // }; - // const viewPortRect: ClientRect = { - // top: 1000, - // bottom: 1300, - // height: 300, - // left: 1000, - // right: 1300, - // width: 300 - // }; - // spyOn(elastic, 'setStyle').and.returnValue({}); - // spyOn(Util, 'getViewportRect').and.returnValue(viewPortRect); - // spyOn(Util, 'getTargetRect').and.returnValue(targetRect); - - // const mockElement = jasmine.createSpyObj('HTMLElement', ['getBoundingClientRect']); - // spyOn(mockElement, 'getBoundingClientRect').and.returnValue(elementRect); - // mockElement.classList = { add: () => { } }; - // mockElement.style = { width: '', height: '' }; - // elastic.position(mockElement, null, null, true); - - // expect(mockElement.style.width).toBe('200px'); - // expect(mockElement.style.height).toBe('100px'); - // }); + it('##6474 - should calculate correctly position', () => { + const elastic: ElasticPositionStrategy = new ElasticPositionStrategy(); + const targetRect = { + top: 100, + bottom: 200, + height: 100, + left: 100, + right: 200, + width: 100 + }; + const elementRect = { + top: 0, + bottom: 300, + height: 300, + left: 0, + right: 300, + width: 300 + }; + const viewPortRect = { + top: 1000, + bottom: 1300, + height: 300, + left: 1000, + right: 1300, + width: 300 + }; + spyOn(elastic, 'setStyle').and.returnValue({}); + spyOn(Util, 'getViewportRect').and.returnValue(viewPortRect); + spyOn(Util, 'getTargetRect').and.returnValue(targetRect); + + const mockElement = jasmine.createSpyObj('HTMLElement', ['getBoundingClientRect']); + spyOn(mockElement, 'getBoundingClientRect').and.returnValue(elementRect); + mockElement.classList = { add: () => { } }; + mockElement.style = { width: '', height: '' }; + elastic.position(mockElement, null, null, true); + + expect(mockElement.style.width).toBe('200px'); + expect(mockElement.style.height).toBe('100px'); + }); it('#16988 - should not reposition overlay when detached', fakeAsync(() => { const fixture = TestBed.createComponent(EmptyPageComponent); @@ -1580,6 +1765,52 @@ describe('igxOverlay', () => { expect(scrollStrat.detach).toHaveBeenCalledTimes(1); })); + + it('Should show dynamic component without ViewContainerRef and log a warning', fakeAsync(() => { + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); + const overlay = fixture.componentInstance.overlay; + + spyOn(console, 'warn'); + + const id = overlay.attach(SimpleDynamicComponent); + overlay.show(id); + tick(DEBOUNCE_TIME); + + expect(console.warn).toHaveBeenCalledWith( + 'Overlay component is created without a ViewContainerRef. The element will be outside the Angular component tree and may not inherit styles. Prefer using the ViewContainerRef overload or provide an outlet.' + ); + + const wrapperElement = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER_MODAL)[0] as HTMLElement; + expect(wrapperElement).toBeDefined(); + const componentElement = wrapperElement.querySelector('test-simple-dynamic-component'); + expect(componentElement).toBeDefined(); + expect(document.body.contains(wrapperElement)).toBeTrue(); + + overlay.detachAll(); + })); + + it('Should append dynamic component without ViewContainerRef to document body', fakeAsync(() => { + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); + const overlay = fixture.componentInstance.overlay; + + const id = overlay.attach(SimpleDynamicComponent); + overlay.show(id); + tick(DEBOUNCE_TIME); + + const overlayInfo = overlay.getOverlayById(id); + expect(overlayInfo).toBeDefined(); + expect(overlayInfo.wrapperElement).toBeDefined(); + expect(overlayInfo.wrapperElement.isConnected).toBeTrue(); + expect(document.body.contains(overlayInfo.wrapperElement)).toBeTrue(); + + const componentEl = overlayInfo.elementRef.nativeElement; + expect(componentEl).toBeDefined(); + expect(document.body.contains(componentEl)).toBeTrue(); + + overlay.detachAll(); + })); }); describe('Integration tests: ', () => { @@ -1610,12 +1841,12 @@ describe('igxOverlay', () => { verticalStartPoint: VerticalAlignment.Top }; overlaySettings.positionStrategy = new GlobalPositionStrategy(positionSettings); - overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, overlaySettings)); tick(); fixture.detectChanges(); - const overlayElement = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - const wrapperElement = overlayElement.children[0]; + const wrapperElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0] as HTMLElement; + expect(wrapperElement).toBeDefined(); expect(wrapperElement).toHaveClass(CLASS_OVERLAY_WRAPPER); overlay.detachAll(); @@ -1712,13 +1943,14 @@ describe('igxOverlay', () => { fixture.detectChanges(); const overlay = fixture.componentInstance.overlay; - overlay.show(overlay.attach(SimpleDynamicComponent)); - tick(); - const componentElement = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByTagName('test-simple-dynamic-component')[0] as HTMLElement; - const componentRect = componentElement.getBoundingClientRect(); - expect((window.innerWidth - componentRect.width) / 2).toBeCloseTo(componentRect.left, 0); - expect((window.innerHeight - componentRect.height) / 2).toBeCloseTo(componentRect.top, 0); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef)); + tick(DEBOUNCE_TIME); + const wrapperElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_MODAL)[0] as HTMLElement; + expect(wrapperElement).toBeDefined(); + expect(wrapperElement).toHaveClass('igx-overlay__wrapper--flex'); + expect(wrapperElement.style.justifyContent).toBe('center'); + expect(wrapperElement.style.alignItems).toBe('center'); overlay.detachAll(); })); @@ -2352,155 +2584,141 @@ describe('igxOverlay', () => { fixture.componentInstance.overlay.detachAll(); })); - // it('Should display each shown component based on the options specified if the component fits into the visible window.', - // fakeAsync(() => { - // const fixture = TestBed.createComponent(EmptyPageComponent); - // fixture.detectChanges(); - - // const overlay = fixture.componentInstance.overlay; - // const button = fixture.componentInstance.buttonElement.nativeElement; - // button.style.left = '150px'; - // button.style.top = '150px'; - // button.style.position = 'relative'; - - // const hAlignmentArray = Object.keys(HorizontalAlignment).filter(key => !isNaN(Number(HorizontalAlignment[key]))); - // const vAlignmentArray = Object.keys(VerticalAlignment).filter(key => !isNaN(Number(VerticalAlignment[key]))); - // hAlignmentArray.forEach(horizontalStartPoint => { - // vAlignmentArray.forEach(verticalStartPoint => { - // hAlignmentArray.forEach(horizontalDirection => { - // // do not check Center as we do nothing here - // if (horizontalDirection === 'Center') { - // return; - // } - // vAlignmentArray.forEach(verticalDirection => { - // // do not check Middle as we do nothing here - // if (verticalDirection === 'Middle') { - // return; - // } - // const positionSettings: PositionSettings = {}; - // positionSettings.horizontalStartPoint = HorizontalAlignment[horizontalStartPoint]; - // positionSettings.verticalStartPoint = VerticalAlignment[verticalStartPoint]; - // positionSettings.horizontalDirection = HorizontalAlignment[horizontalDirection]; - // positionSettings.verticalDirection = VerticalAlignment[verticalDirection]; - - // const overlaySettings: OverlaySettings = { - // target: button, - // positionStrategy: new AutoPositionStrategy(positionSettings), - // modal: false, - // closeOnOutsideClick: false - // }; - - // overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); - // tick(); - // fixture.detectChanges(); - - // const targetRect = (overlaySettings.target as HTMLElement).getBoundingClientRect(); - // const componentElement = (fixture.debugElement.nativeElement as HTMLElement) - // .parentElement.getElementsByTagName('test-simple-dynamic-component')[0]; - // const componentRect = componentElement.getBoundingClientRect(); - // const screenRect: ClientRect = { - // left: 0, - // top: 0, - // right: window.innerWidth, - // bottom: window.innerHeight, - // width: window.innerWidth, - // height: window.innerHeight, - // }; - - // const location = getOverlayWrapperLocation(positionSettings, targetRect, componentRect, screenRect); - // expect(componentRect.top.toFixed(1)).toEqual(location.y.toFixed(1)); - // expect(componentRect.left.toFixed(1)).toEqual(location.x.toFixed(1)); - // expect(document.body.scrollHeight > document.body.clientHeight).toBeFalsy(); // check scrollbar - // fixture.componentInstance.overlay.detachAll(); - // tick(); - // fixture.detectChanges(); - // }); - // }); - // }); - // }); - // })); - - // it(`Should reposition the component and render it correctly in the window, even when the rendering options passed - // should result in otherwise a partially hidden component. No scrollbars should appear.`, - // fakeAsync(() => { - // const fixture = TestBed.createComponent(EmptyPageComponent); - // fixture.detectChanges(); - - // const button = fixture.componentInstance.buttonElement.nativeElement; - // const overlay = fixture.componentInstance.overlay; - // button.style.position = 'relative'; - // button.style.width = '50px'; - // button.style.height = '50px'; - // const buttonLocations = [ - // { left: `0px`, top: `0px` }, // topLeft - // { left: `${window.innerWidth - 200}px`, top: `0px` }, // topRight - // { left: `0px`, top: `${window.innerHeight - 200}px` }, // bottomLeft - // { left: `${window.innerWidth - 200}px`, top: `${window.innerHeight - 200}px` } // bottomRight - // ]; - // const hAlignmentArray = Object.keys(HorizontalAlignment).filter(key => !isNaN(Number(HorizontalAlignment[key]))); - // const vAlignmentArray = Object.keys(VerticalAlignment).filter(key => !isNaN(Number(VerticalAlignment[key]))); - // for (const buttonLocation of buttonLocations) { - // for (const horizontalStartPoint of hAlignmentArray) { - // for (const verticalStartPoint of vAlignmentArray) { - // for (const horizontalDirection of hAlignmentArray) { - // for (const verticalDirection of vAlignmentArray) { - - // const positionSettings: PositionSettings = {}; - // button.style.left = buttonLocation.left; - // button.style.top = buttonLocation.top; - - // positionSettings.horizontalStartPoint = HorizontalAlignment[horizontalStartPoint]; - // positionSettings.verticalStartPoint = VerticalAlignment[verticalStartPoint]; - // positionSettings.horizontalDirection = HorizontalAlignment[horizontalDirection]; - // positionSettings.verticalDirection = VerticalAlignment[verticalDirection]; - - // const overlaySettings: OverlaySettings = { - // target: button, - // positionStrategy: new AutoPositionStrategy(positionSettings), - // modal: false, - // closeOnOutsideClick: false - // }; - - // overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); - // tick(); - // fixture.detectChanges(); - - // const targetRect = (overlaySettings.target as HTMLElement).getBoundingClientRect(); - // const contentElement = (fixture.nativeElement as HTMLElement) - // .parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0] as HTMLElement; - // const contentRect = contentElement.getBoundingClientRect(); - // const screenRect: ClientRect = { - // left: 0, - // top: 0, - // right: window.innerWidth, - // bottom: window.innerHeight, - // width: window.innerWidth, - // height: window.innerHeight, - // }; - - // const loc = getOverlayWrapperLocation(positionSettings, targetRect, contentRect, screenRect); - // expect(contentRect.top.toFixed(1)) - // .withContext(`YYY HD: ${horizontalDirection}; VD: ${verticalDirection}; ` + - // `HSP: ${horizontalStartPoint}; VSP: ${verticalStartPoint}; ` + - // `BL: ${buttonLocation.left}; BT: ${buttonLocation.top}; ` + - // `STYLE: ${contentElement.getAttribute('style')};`) - // .toEqual(loc.y.toFixed(1)); - // expect(contentRect.left.toFixed(1)) - // .withContext(`XXX HD: ${horizontalDirection}; VD: ${verticalDirection}; ` + - // `HSP: ${horizontalStartPoint}; VSP: ${verticalStartPoint}; ` + - // `BL: ${buttonLocation.left}; BT: ${buttonLocation.top}; ` + - // `STYLE: ${contentElement.getAttribute('style')};`) - // .toEqual(loc.x.toFixed(1)); - // expect(document.body.scrollHeight > document.body.clientHeight).toBeFalsy(); // check scrollbar - // fixture.componentInstance.overlay.detachAll(); - // tick(); - // fixture.detectChanges(); - // } - // } - // } - // } - // } - // })); + it('Should display each shown component based on the options specified if the component fits into the visible window.', + fakeAsync(() => { + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); + + const overlay = fixture.componentInstance.overlay; + const button = fixture.componentInstance.buttonElement.nativeElement; + button.style.left = '150px'; + button.style.top = '150px'; + button.style.position = 'relative'; + + const hAlignmentArray = Object.keys(HorizontalAlignment).filter(key => !isNaN(Number(HorizontalAlignment[key]))); + const vAlignmentArray = Object.keys(VerticalAlignment).filter(key => !isNaN(Number(VerticalAlignment[key]))); + hAlignmentArray.forEach(horizontalStartPoint => { + vAlignmentArray.forEach(verticalStartPoint => { + hAlignmentArray.forEach(horizontalDirection => { + // do not check Center as we do nothing here + if (horizontalDirection === 'Center') { + return; + } + vAlignmentArray.forEach(verticalDirection => { + // do not check Middle as we do nothing here + if (verticalDirection === 'Middle') { + return; + } + const positionSettings: PositionSettings = {}; + positionSettings.horizontalStartPoint = HorizontalAlignment[horizontalStartPoint]; + positionSettings.verticalStartPoint = VerticalAlignment[verticalStartPoint]; + positionSettings.horizontalDirection = HorizontalAlignment[horizontalDirection]; + positionSettings.verticalDirection = VerticalAlignment[verticalDirection]; + + const overlaySettings: OverlaySettings = { + target: button, + positionStrategy: new AutoPositionStrategy(positionSettings), + modal: false, + closeOnOutsideClick: false + }; + + overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); + tick(); + fixture.detectChanges(); + + const targetRect = (overlaySettings.target as HTMLElement).getBoundingClientRect(); + const componentElement = (fixture.debugElement.nativeElement as HTMLElement) + .parentElement.getElementsByTagName('test-simple-dynamic-component')[0]; + const componentRect = componentElement.getBoundingClientRect(); + const screenRect = new DOMRect(0, 0, window.innerWidth, window.innerHeight); + + const location = getOverlayWrapperLocation(positionSettings, targetRect, componentRect, screenRect); + expect(componentRect.top.toFixed(1)).toEqual(location.y.toFixed(1)); + expect(componentRect.left.toFixed(1)).toEqual(location.x.toFixed(1)); + expect(document.body.scrollHeight > document.body.clientHeight).toBeFalsy(); // check scrollbar + fixture.componentInstance.overlay.detachAll(); + tick(); + fixture.detectChanges(); + }); + }); + }); + }); + })); + + it(`Should reposition the component and render it correctly in the window, even when the rendering options passed + should result in otherwise a partially hidden component. No scrollbars should appear.`, + fakeAsync(() => { + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); + + const button = fixture.componentInstance.buttonElement.nativeElement; + const overlay = fixture.componentInstance.overlay; + button.style.position = 'relative'; + button.style.width = '50px'; + button.style.height = '50px'; + const buttonLocations = [ + { left: `0px`, top: `0px` }, // topLeft + { left: `${window.innerWidth - 200}px`, top: `0px` }, // topRight + { left: `0px`, top: `${window.innerHeight - 200}px` }, // bottomLeft + { left: `${window.innerWidth - 200}px`, top: `${window.innerHeight - 200}px` } // bottomRight + ]; + const hAlignmentArray = Object.keys(HorizontalAlignment).filter(key => !isNaN(Number(HorizontalAlignment[key]))); + const vAlignmentArray = Object.keys(VerticalAlignment).filter(key => !isNaN(Number(VerticalAlignment[key]))); + for (const buttonLocation of buttonLocations) { + for (const horizontalStartPoint of hAlignmentArray) { + for (const verticalStartPoint of vAlignmentArray) { + for (const horizontalDirection of hAlignmentArray) { + for (const verticalDirection of vAlignmentArray) { + + const positionSettings: PositionSettings = {}; + button.style.left = buttonLocation.left; + button.style.top = buttonLocation.top; + + positionSettings.horizontalStartPoint = HorizontalAlignment[horizontalStartPoint]; + positionSettings.verticalStartPoint = VerticalAlignment[verticalStartPoint]; + positionSettings.horizontalDirection = HorizontalAlignment[horizontalDirection]; + positionSettings.verticalDirection = VerticalAlignment[verticalDirection]; + + const overlaySettings: OverlaySettings = { + target: button, + positionStrategy: new AutoPositionStrategy(positionSettings), + modal: false, + closeOnOutsideClick: false + }; + + overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); + tick(); + fixture.detectChanges(); + + const targetRect = (overlaySettings.target as HTMLElement).getBoundingClientRect(); + const contentElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0] as HTMLElement; + const contentRect = contentElement.getBoundingClientRect(); + const screenRect = new DOMRect(0, 0, window.innerWidth, window.innerHeight); + + const loc = getOverlayWrapperLocation(positionSettings, targetRect, contentRect, screenRect); + expect(contentRect.top.toFixed(1)) + .withContext(`YYY HD: ${horizontalDirection}; VD: ${verticalDirection}; ` + + `HSP: ${horizontalStartPoint}; VSP: ${verticalStartPoint}; ` + + `BL: ${buttonLocation.left}; BT: ${buttonLocation.top}; ` + + `STYLE: ${contentElement.getAttribute('style')};`) + .toEqual(loc.y.toFixed(1)); + expect(contentRect.left.toFixed(1)) + .withContext(`XXX HD: ${horizontalDirection}; VD: ${verticalDirection}; ` + + `HSP: ${horizontalStartPoint}; VSP: ${verticalStartPoint}; ` + + `BL: ${buttonLocation.left}; BT: ${buttonLocation.top}; ` + + `STYLE: ${contentElement.getAttribute('style')};`) + .toEqual(loc.x.toFixed(1)); + expect(document.body.scrollHeight > document.body.clientHeight).toBeFalsy(); // check scrollbar + fixture.componentInstance.overlay.detachAll(); + tick(); + fixture.detectChanges(); + } + } + } + } + } + })); it('Should render margins correctly.', fakeAsync(() => { const expectedMargin = '0px'; @@ -2830,177 +3048,169 @@ describe('igxOverlay', () => { fixture.componentInstance.overlay.detachAll(); })); - // it('Should display each shown component based on the options specified if the component fits into the visible window.', - // fakeAsync(() => { - // const fixture = TestBed.createComponent(EmptyPageComponent); - // fixture.detectChanges(); - - // const overlay = fixture.componentInstance.overlay; - // const button = fixture.componentInstance.buttonElement.nativeElement; - // button.style.left = '150px'; - // button.style.top = '150px'; - // button.style.position = 'relative'; - - // const hAlignmentArray = Object.keys(HorizontalAlignment).filter(key => !isNaN(Number(HorizontalAlignment[key]))); - // const vAlignmentArray = Object.keys(VerticalAlignment).filter(key => !isNaN(Number(VerticalAlignment[key]))); - // hAlignmentArray.forEach(horizontalStartPoint => { - // vAlignmentArray.forEach(verticalStartPoint => { - // hAlignmentArray.forEach(horizontalDirection => { - // vAlignmentArray.forEach(verticalDirection => { - // const positionSettings: PositionSettings = {}; - // positionSettings.horizontalStartPoint = HorizontalAlignment[horizontalStartPoint]; - // positionSettings.verticalStartPoint = VerticalAlignment[verticalStartPoint]; - // positionSettings.horizontalDirection = HorizontalAlignment[horizontalDirection]; - // positionSettings.verticalDirection = VerticalAlignment[verticalDirection]; - // positionSettings.minSize = { width: 80, height: 80 }; - - // const overlaySettings: OverlaySettings = { - // target: button, - // positionStrategy: new ElasticPositionStrategy(positionSettings), - // modal: false, - // closeOnOutsideClick: false - // }; - - // overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); - // tick(); - // fixture.detectChanges(); - - // const targetRect = (overlaySettings.target as HTMLElement).getBoundingClientRect() as ClientRect; - // // we need original rect of the wrapper element. After it was shown in overlay elastic may - // // set width and/or height. To get original rect remove width and height, get the rect and - // // restore width and height; - // const contentElement = (fixture.nativeElement as HTMLElement) - // .parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0] as HTMLElement; - // const width = contentElement.style.width; - // contentElement.style.width = ''; - // const height = contentElement.style.height; - // contentElement.style.height = ''; - // let contentRect = contentElement.getBoundingClientRect() as ClientRect; - // contentElement.style.width = width; - // contentElement.style.height = height; - // const screenRect: ClientRect = { - // left: 0, - // top: 0, - // right: window.innerWidth, - // bottom: window.innerHeight, - // width: window.innerWidth, - // height: window.innerHeight, - // }; - - // const location = - // getOverlayWrapperLocation(positionSettings, targetRect, contentRect, screenRect, true); - // // now get the wrapper rect as it is after elastic was applied - // contentRect = contentElement.getBoundingClientRect() as ClientRect; - // expect(contentRect.top.toFixed(1)) - // .withContext(`HD: ${horizontalDirection}; HSP: ${horizontalStartPoint};` + - // `VD: ${verticalDirection}; VSP: ${verticalStartPoint};` + - // `STYLE: ${contentElement.getAttribute('style')};`) - // .toEqual(location.y.toFixed(1)); - // expect(contentRect.left.toFixed(1)) - // .withContext(`HD: ${horizontalDirection}; HSP: ${horizontalStartPoint};` + - // `VD: ${verticalDirection}; VSP: ${verticalStartPoint};` + - // `STYLE: ${contentElement.getAttribute('style')};`) - // .toEqual(location.x.toFixed(1)); - // fixture.componentInstance.overlay.detachAll(); - // tick(); - // fixture.detectChanges(); - // }); - // }); - // }); - // }); - // })); - - // it(`Should reposition the component and render it correctly in the window, even when the rendering options passed - // should result in otherwise a partially hidden component.No scrollbars should appear.`, - // fakeAsync(() => { - // const fixture = TestBed.createComponent(EmptyPageComponent); - // fixture.detectChanges(); - - // const overlay = fixture.componentInstance.overlay; - // const button = fixture.componentInstance.buttonElement.nativeElement; - // button.style.position = 'relative'; - // button.style.width = '50px'; - // button.style.height = '50px'; - // const buttonLocations = [ - // { left: `0px`, top: `0px` }, // topLeft - // { left: `${window.innerWidth - button.width} px`, top: `0px` }, // topRight - // { left: `0px`, top: `${window.innerHeight - button.height} px` }, // bottomLeft - // { left: `${window.innerWidth - button.width} px`, top: `${window.innerHeight - button.height} px` } // bottomRight - // ]; - // const hAlignmentArray = Object.keys(HorizontalAlignment).filter(key => !isNaN(Number(HorizontalAlignment[key]))); - // const vAlignmentArray = Object.keys(VerticalAlignment).filter(key => !isNaN(Number(VerticalAlignment[key]))); - // for (const buttonLocation of buttonLocations) { - // for (const horizontalStartPoint of hAlignmentArray) { - // for (const verticalStartPoint of vAlignmentArray) { - // for (const horizontalDirection of hAlignmentArray) { - // for (const verticalDirection of vAlignmentArray) { - // const positionSettings: PositionSettings = {}; - // button.style.left = buttonLocation.left; - // button.style.top = buttonLocation.top; - - // positionSettings.horizontalStartPoint = HorizontalAlignment[horizontalStartPoint]; - // positionSettings.verticalStartPoint = VerticalAlignment[verticalStartPoint]; - // positionSettings.horizontalDirection = HorizontalAlignment[horizontalDirection]; - // positionSettings.verticalDirection = VerticalAlignment[verticalDirection]; - // positionSettings.minSize = { width: 80, height: 80 }; - - // const overlaySettings: OverlaySettings = { - // target: button, - // positionStrategy: new ElasticPositionStrategy(positionSettings), - // modal: false, - // closeOnOutsideClick: false - // }; - - // overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); - // tick(); - // fixture.detectChanges(); - - // const targetRect = (overlaySettings.target as HTMLElement).getBoundingClientRect() as ClientRect; - // // we need original rect of the wrapper element. After it was shown in overlay elastic may - // // set width and/or height. To get original rect remove width and height, get the rect and - // // restore width and height; - // const contentElement = (fixture.nativeElement as HTMLElement) - // .parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0] as HTMLElement; - // const width = contentElement.style.width; - // contentElement.style.width = ''; - // const height = contentElement.style.height; - // contentElement.style.height = ''; - // let contentRect = contentElement.getBoundingClientRect(); - // contentElement.style.width = width; - // contentElement.style.height = height; - // const screenRect: ClientRect = { - // left: 0, - // top: 0, - // right: window.innerWidth, - // bottom: window.innerHeight, - // width: window.innerWidth, - // height: window.innerHeight, - // }; - - // const loc = - // getOverlayWrapperLocation(positionSettings, targetRect, contentRect, screenRect, true); - // // now get the wrapper rect as it is after elastic was applied - // contentRect = contentElement.getBoundingClientRect(); - // expect(contentRect.top.toFixed(1)) - // .withContext(`HD: ${horizontalDirection}; HSP: ${horizontalStartPoint};` + - // `VD: ${verticalDirection}; VSP: ${verticalStartPoint};` + - // `STYLE: ${contentElement.getAttribute('style')};`) - // .toEqual(loc.y.toFixed(1)); - // expect(contentRect.left.toFixed(1)) - // .withContext(`HD: ${horizontalDirection}; HSP: ${horizontalStartPoint};` + - // `VD: ${verticalDirection}; VSP: ${verticalStartPoint}` + - // `STYLE: ${contentElement.getAttribute('style')};`) - // .toEqual(loc.x.toFixed(1)); - // expect(document.body.scrollHeight > document.body.clientHeight).toBeFalsy(); // check scrollbars - // fixture.componentInstance.overlay.detachAll(); - // tick(); - // fixture.detectChanges(); - // } - // } - // } - // } - // } - // })); + it('Should display each shown component based on the options specified if the component fits into the visible window.', + fakeAsync(() => { + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); + + const overlay = fixture.componentInstance.overlay; + const button = fixture.componentInstance.buttonElement.nativeElement; + button.style.left = '150px'; + button.style.top = '150px'; + button.style.position = 'relative'; + + const hAlignmentArray = Object.keys(HorizontalAlignment).filter(key => !isNaN(Number(HorizontalAlignment[key]))); + const vAlignmentArray = Object.keys(VerticalAlignment).filter(key => !isNaN(Number(VerticalAlignment[key]))); + hAlignmentArray.forEach(horizontalStartPoint => { + vAlignmentArray.forEach(verticalStartPoint => { + hAlignmentArray.forEach(horizontalDirection => { + vAlignmentArray.forEach(verticalDirection => { + const positionSettings: PositionSettings = {}; + positionSettings.horizontalStartPoint = HorizontalAlignment[horizontalStartPoint]; + positionSettings.verticalStartPoint = VerticalAlignment[verticalStartPoint]; + positionSettings.horizontalDirection = HorizontalAlignment[horizontalDirection]; + positionSettings.verticalDirection = VerticalAlignment[verticalDirection]; + positionSettings.minSize = { width: 80, height: 80 }; + + const overlaySettings: OverlaySettings = { + target: button, + positionStrategy: new ElasticPositionStrategy(positionSettings), + modal: false, + closeOnOutsideClick: false + }; + + overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); + tick(); + fixture.detectChanges(); + + const targetRect = (overlaySettings.target as HTMLElement).getBoundingClientRect(); + // we need original rect of the wrapper element. After it was shown in overlay elastic may + // set width and/or height. To get original rect remove width and height, get the rect and + // restore width and height. Also hide overflow to prevent scrollbar-induced dimension changes. + const contentElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0] as HTMLElement; + const width = contentElement.style.width; + contentElement.style.width = ''; + const height = contentElement.style.height; + contentElement.style.height = ''; + contentElement.style.overflow = 'hidden'; + let contentRect = contentElement.getBoundingClientRect(); + contentElement.style.width = width; + contentElement.style.height = height; + const screenRect = new DOMRect(0, 0, window.innerWidth, window.innerHeight); + + const location = + getOverlayWrapperLocation(positionSettings, targetRect, contentRect, screenRect, true); + // now get the wrapper rect as it is after elastic was applied + // keep overflow hidden to avoid scrollbar-induced height changes affecting position + contentRect = contentElement.getBoundingClientRect(); + contentElement.style.overflow = ''; + expect(contentRect.top.toFixed(1)) + .withContext(`HD: ${horizontalDirection}; HSP: ${horizontalStartPoint};` + + `VD: ${verticalDirection}; VSP: ${verticalStartPoint};` + + `STYLE: ${contentElement.getAttribute('style')};`) + .toEqual(location.y.toFixed(1)); + expect(contentRect.left.toFixed(1)) + .withContext(`HD: ${horizontalDirection}; HSP: ${horizontalStartPoint};` + + `VD: ${verticalDirection}; VSP: ${verticalStartPoint};` + + `STYLE: ${contentElement.getAttribute('style')};`) + .toEqual(location.x.toFixed(1)); + fixture.componentInstance.overlay.detachAll(); + tick(); + fixture.detectChanges(); + }); + }); + }); + }); + })); + + it(`Should reposition the component and render it correctly in the window, even when the rendering options passed + should result in otherwise a partially hidden component. No scrollbars should appear.`, + fakeAsync(() => { + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); + + const overlay = fixture.componentInstance.overlay; + const button = fixture.componentInstance.buttonElement.nativeElement; + button.style.position = 'relative'; + button.style.width = '50px'; + button.style.height = '50px'; + const buttonLocations = [ + { left: `0px`, top: `0px` }, // topLeft + { left: `${window.innerWidth - 200}px`, top: `0px` }, // topRight + { left: `0px`, top: `${window.innerHeight - 200}px` }, // bottomLeft + { left: `${window.innerWidth - 200}px`, top: `${window.innerHeight - 200}px` } // bottomRight + ]; + const hAlignmentArray = Object.keys(HorizontalAlignment).filter(key => !isNaN(Number(HorizontalAlignment[key]))); + const vAlignmentArray = Object.keys(VerticalAlignment).filter(key => !isNaN(Number(VerticalAlignment[key]))); + for (const buttonLocation of buttonLocations) { + for (const horizontalStartPoint of hAlignmentArray) { + for (const verticalStartPoint of vAlignmentArray) { + for (const horizontalDirection of hAlignmentArray) { + for (const verticalDirection of vAlignmentArray) { + const positionSettings: PositionSettings = {}; + button.style.left = buttonLocation.left; + button.style.top = buttonLocation.top; + + positionSettings.horizontalStartPoint = HorizontalAlignment[horizontalStartPoint]; + positionSettings.verticalStartPoint = VerticalAlignment[verticalStartPoint]; + positionSettings.horizontalDirection = HorizontalAlignment[horizontalDirection]; + positionSettings.verticalDirection = VerticalAlignment[verticalDirection]; + positionSettings.minSize = { width: 80, height: 80 }; + + const overlaySettings: OverlaySettings = { + target: button, + positionStrategy: new ElasticPositionStrategy(positionSettings), + modal: false, + closeOnOutsideClick: false + }; + + overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); + tick(); + fixture.detectChanges(); + + const targetRect = (overlaySettings.target as HTMLElement).getBoundingClientRect(); + // we need original rect of the wrapper element. After it was shown in overlay elastic may + // set width and/or height. To get original rect remove width and height, get the rect and + // restore width and height. Also hide overflow to prevent scrollbar-induced dimension changes. + const contentElement = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0] as HTMLElement; + const width = contentElement.style.width; + contentElement.style.width = ''; + const height = contentElement.style.height; + contentElement.style.height = ''; + contentElement.style.overflow = 'hidden'; + let contentRect = contentElement.getBoundingClientRect(); + contentElement.style.width = width; + contentElement.style.height = height; + const screenRect = new DOMRect(0, 0, window.innerWidth, window.innerHeight); + + const loc = + getOverlayWrapperLocation(positionSettings, targetRect, contentRect, screenRect, true); + // now get the wrapper rect as it is after elastic was applied + // keep overflow hidden to avoid scrollbar-induced height changes affecting position + contentRect = contentElement.getBoundingClientRect(); + contentElement.style.overflow = ''; + expect(contentRect.top.toFixed(1)) + .withContext(`HD: ${horizontalDirection}; HSP: ${horizontalStartPoint};` + + `VD: ${verticalDirection}; VSP: ${verticalStartPoint};` + + `STYLE: ${contentElement.getAttribute('style')};`) + .toEqual(loc.y.toFixed(1)); + expect(contentRect.left.toFixed(1)) + .withContext(`HD: ${horizontalDirection}; HSP: ${horizontalStartPoint};` + + `VD: ${verticalDirection}; VSP: ${verticalStartPoint};` + + `STYLE: ${contentElement.getAttribute('style')};`) + .toEqual(loc.x.toFixed(1)); + expect(document.body.scrollHeight > document.body.clientHeight).toBeFalsy(); // check scrollbars + fixture.componentInstance.overlay.detachAll(); + tick(); + fixture.detectChanges(); + } + } + } + } + } + })); it('Should render margins correctly.', fakeAsync(() => { const expectedMargin = '0px'; @@ -3522,26 +3732,28 @@ describe('igxOverlay', () => { it('Should close the opened overlays consecutively on esc keypress', fakeAsync(() => { const fixture = TestBed.createComponent(EmptyPageComponent); const overlay = fixture.componentInstance.overlay; - overlay.show(overlay.attach(SimpleDynamicComponent, { closeOnEscape: true })); - tick(); - overlay.show(overlay.attach(SimpleDynamicComponent, { closeOnEscape: true })); - tick(); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, { closeOnEscape: true })); + tick(DEBOUNCE_TIME); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, { closeOnEscape: true })); + tick(DEBOUNCE_TIME); - const overlayElement = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayElement.children.length).toBe(2); - expect((overlayElement.children[0] as HTMLElement).style.visibility).toEqual(''); - expect((overlayElement.children[1] as HTMLElement).style.visibility).toEqual(''); + const wrappers = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_MODAL); + expect(wrappers.length).toBe(2); + const wrapper0 = wrappers[0] as HTMLElement; + const wrapper1 = wrappers[1] as HTMLElement; + expect(wrapper0.style.visibility).toEqual(''); + expect(wrapper1.style.visibility).toEqual(''); UIInteractions.triggerKeyDownEvtUponElem('Escape', document); tick(); - expect((overlayElement.children[0] as HTMLElement).style.visibility).toEqual(''); - expect((overlayElement.children[1] as HTMLElement).style.visibility).toEqual('hidden'); + expect(wrapper0.style.visibility).toEqual(''); + expect(wrapper1.style.visibility).toEqual('hidden'); UIInteractions.triggerKeyDownEvtUponElem('Escape', document); tick(); - expect((overlayElement.children[0] as HTMLElement).style.visibility).toEqual('hidden'); - expect((overlayElement.children[1] as HTMLElement).style.visibility).toEqual('hidden'); + expect(wrapper0.style.visibility).toEqual('hidden'); + expect(wrapper1.style.visibility).toEqual('hidden'); overlay.detachAll(); })); @@ -3549,31 +3761,34 @@ describe('igxOverlay', () => { it('Should not close the opened overlays consecutively on esc keypress', fakeAsync(() => { const fixture = TestBed.createComponent(EmptyPageComponent); const overlay = fixture.componentInstance.overlay; - overlay.show(overlay.attach(SimpleDynamicComponent, { closeOnEscape: true })); - tick(); - overlay.show(overlay.attach(SimpleDynamicComponent, { closeOnEscape: false })); - tick(); - overlay.show(overlay.attach(SimpleDynamicComponent, { closeOnEscape: true })); - tick(); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, { closeOnEscape: true })); + tick(DEBOUNCE_TIME); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, { closeOnEscape: false })); + tick(DEBOUNCE_TIME); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, { closeOnEscape: true })); + tick(DEBOUNCE_TIME); - const overlayElement = (fixture.nativeElement as HTMLElement) - .parentElement.getElementsByClassName(CLASS_OVERLAY_MAIN)[0] as HTMLElement; - expect(overlayElement.children.length).toBe(3); - expect((overlayElement.children[0] as HTMLElement).style.visibility).toEqual(''); - expect((overlayElement.children[1] as HTMLElement).style.visibility).toEqual(''); - expect((overlayElement.children[2] as HTMLElement).style.visibility).toEqual(''); + const wrappers = (fixture.nativeElement as HTMLElement) + .parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER_MODAL); + expect(wrappers.length).toBe(3); + const wrapper0 = wrappers[0] as HTMLElement; + const wrapper1 = wrappers[1] as HTMLElement; + const wrapper2 = wrappers[2] as HTMLElement; + expect(wrapper0.style.visibility).toEqual(''); + expect(wrapper1.style.visibility).toEqual(''); + expect(wrapper2.style.visibility).toEqual(''); UIInteractions.triggerKeyDownEvtUponElem('Escape', document); tick(); - expect((overlayElement.children[0] as HTMLElement).style.visibility).toEqual(''); - expect((overlayElement.children[1] as HTMLElement).style.visibility).toEqual(''); - expect((overlayElement.children[2] as HTMLElement).style.visibility).toEqual('hidden'); + expect(wrapper0.style.visibility).toEqual(''); + expect(wrapper1.style.visibility).toEqual(''); + expect(wrapper2.style.visibility).toEqual('hidden'); UIInteractions.triggerKeyDownEvtUponElem('Escape', document); tick(); - expect((overlayElement.children[0] as HTMLElement).style.visibility).toEqual(''); - expect((overlayElement.children[1] as HTMLElement).style.visibility).toEqual(''); - expect((overlayElement.children[2] as HTMLElement).style.visibility).toEqual('hidden'); + expect(wrapper0.style.visibility).toEqual(''); + expect(wrapper1.style.visibility).toEqual(''); + expect(wrapper2.style.visibility).toEqual('hidden'); overlay.detachAll(); })); @@ -4441,9 +4656,9 @@ describe('igxOverlay', () => { scrollStrategy }; - overlay.show(overlay.attach(SimpleDynamicComponent, overlaySettings)); + overlay.show(overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, overlaySettings)); tick(); - expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); + expect((fixture.nativeElement as HTMLElement).parentElement.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); const componentElement = (fixture.debugElement.nativeElement as HTMLElement) .parentElement.getElementsByTagName('test-simple-dynamic-component')[0]; @@ -4734,7 +4949,7 @@ export class EmptyPageComponent { @ViewChild('div', { static: true }) public divElement: ElementRef; public click() { - this.overlay.show(this.overlay.attach(SimpleDynamicComponent)); + this.overlay.show(this.overlay.attach(SimpleDynamicComponent, this.viewContainerRef)); } } @@ -4769,6 +4984,7 @@ export class EmptyPageInShadowDomComponent { }) export class DownRightButtonComponent { public overlay = inject(IgxOverlayService); + public viewContainerRef = inject(ViewContainerRef); @ViewChild('button', { static: true }) public buttonElement: ElementRef; @@ -4785,7 +5001,7 @@ export class DownRightButtonComponent { public click() { this.positionStrategy.settings = this.ButtonPositioningSettings; - this.overlay.show(this.overlay.attach(SimpleDynamicComponent, { + this.overlay.show(this.overlay.attach(SimpleDynamicComponent, this.viewContainerRef, { target: this.target, positionStrategy: this.positionStrategy, scrollStrategy: new NoOpScrollStrategy(), @@ -4809,12 +5025,12 @@ export class DownRightButtonComponent { }) export class TopLeftOffsetComponent { public overlay = inject(IgxOverlayService); - + public viewContainerRef = inject(ViewContainerRef); @ViewChild('button', { static: true }) public buttonElement: ElementRef; public click() { - this.overlay.show(this.overlay.attach(SimpleDynamicComponent)); + this.overlay.show(this.overlay.attach(SimpleDynamicComponent, this.viewContainerRef)); } } @@ -4830,15 +5046,16 @@ export class TopLeftOffsetComponent { }) export class TwoButtonsComponent { public overlay = inject(IgxOverlayService); + public viewContainerRef = inject(ViewContainerRef); public settings: OverlaySettings = { modal: false }; public clickOne() { - this.overlay.show(this.overlay.attach(SimpleDynamicComponent), this.settings); + this.overlay.show(this.overlay.attach(SimpleDynamicComponent, this.viewContainerRef), this.settings); } public clickTwo() { - this.overlay.show(this.overlay.attach(SimpleDynamicComponent), this.settings); + this.overlay.show(this.overlay.attach(SimpleDynamicComponent, this.viewContainerRef), this.settings); } public divClick(ev: Event) { @@ -4937,11 +5154,12 @@ export class ScrollableComponent { }) export class FlexContainerComponent { public overlay = inject(IgxOverlayService); + public viewContainerRef = inject(ViewContainerRef); @ViewChild('button', { static: true }) public buttonElement: ElementRef; public overlaySettings: OverlaySettings = {}; public click() { - this.overlay.show(this.overlay.attach(SimpleDynamicComponent), this.overlaySettings); + this.overlay.show(this.overlay.attach(SimpleDynamicComponent, this.viewContainerRef), this.overlaySettings); } } diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.ts index 79c36014422..793db60c791 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.ts @@ -117,7 +117,6 @@ export class IgxOverlayService implements OnDestroy { private _componentId = 0; private _overlayInfos: OverlayInfo[] = []; - private _overlayElement: HTMLElement; private _document: Document; private _keyPressEventListener: Subscription; private destroy$ = new Subject(); @@ -330,13 +329,18 @@ export class IgxOverlayService implements OnDestroy { // Append the content to the overlay info.settings = eventArgs.settings; this._overlayInfos.push(info); - info.hook = this.placeElementHook(info.elementRef.nativeElement); const elementRect = info.elementRef.nativeElement.getBoundingClientRect(); info.initialSize = { width: elementRect.width, height: elementRect.height }; // Get the size before moving the container into the overlay so that it does not forget about inherited styles. this.getComponentSize(info); - this.moveElementToOverlay(info); - // Update the container size after moving if there is size. + if (info.settings.outlet) { + this.moveElementToOutlet(info); + } else if (info.elementRef.nativeElement.parentElement) { + this.wrapElementInPlace(info); + } else { + this.appendElementToDocument(info); + } + // Update the container size after wrapping/moving if there is size. if (info.size) { info.elementRef.nativeElement.parentElement.style.setProperty('--ig-size', info.size); } @@ -619,6 +623,7 @@ export class IgxOverlayService implements OnDestroy { if (createSettings) { ({ injector: elementInjector, ...overlaySettings } = createSettings); } + console.warn('Overlay component is created without a ViewContainerRef. The element will be outside the Angular component tree and may not inherit styles. Prefer using the ViewContainerRef overload or provide an outlet.'); dynamicComponent = createComponent(component, { environmentInjector, elementInjector }); this._appRef.attachView(dynamicComponent.hostView); } @@ -649,13 +654,32 @@ export class IgxOverlayService implements OnDestroy { return hook; } - private moveElementToOverlay(info: OverlayInfo) { + private moveElementToOutlet(info: OverlayInfo) { + info.hook = this.placeElementHook(info.elementRef.nativeElement); info.wrapperElement = this.getWrapperElement(); const contentElement = this.getContentElement(info.wrapperElement, info.settings.modal); - this.getOverlayElement(info).appendChild(info.wrapperElement); + const outlet = info.settings.outlet.nativeElement || info.settings.outlet; + outlet.appendChild(info.wrapperElement); contentElement.appendChild(info.elementRef.nativeElement); } + private wrapElementInPlace(info: OverlayInfo) { + info.wrapperElement = this.getWrapperElement(); + const contentElement = this.getContentElement(info.wrapperElement, info.settings.modal); + const element = info.elementRef.nativeElement; + // Insert wrapper where element currently is, then move element inside the content div + element.parentElement.insertBefore(info.wrapperElement, element); + contentElement.appendChild(element); + } + + private appendElementToDocument(info: OverlayInfo) { + info.wrapperElement = this.getWrapperElement(); + const contentElement = this.getContentElement(info.wrapperElement, info.settings.modal); + this._document.body.appendChild(info.wrapperElement); + contentElement.appendChild(info.elementRef.nativeElement); + info.appendedToBody = true; + } + private getWrapperElement(): HTMLElement { const wrapper: HTMLElement = this._document.createElement('div'); wrapper.classList.add('igx-overlay__wrapper'); @@ -684,18 +708,6 @@ export class IgxOverlayService implements OnDestroy { return content; } - private getOverlayElement(info: OverlayInfo): HTMLElement { - if (info.settings.outlet) { - return info.settings.outlet.nativeElement || info.settings.outlet; - } - if (!this._overlayElement) { - this._overlayElement = this._document.createElement('div'); - this._overlayElement.classList.add('igx-overlay'); - this._document.body.appendChild(this._overlayElement); - } - return this._overlayElement; - } - private updateSize(info: OverlayInfo) { if (info.componentRef) { // if we are positioning component this is first time it gets visible @@ -734,11 +746,23 @@ export class IgxOverlayService implements OnDestroy { private cleanUp(info: OverlayInfo) { const child: HTMLElement = info.elementRef.nativeElement; - const outlet = this.getOverlayElement(info); - // if same element is shown in other overlay outlet will not contain - // the element and we should not remove it form outlet - if (outlet.contains(child)) { - outlet.removeChild(child.parentNode.parentNode); + if (info.appendedToBody) { + // Element was appended to document body as a fallback (no outlet, no parent). + // Just remove the wrapper; the dynamic component will be destroyed below. + info.wrapperElement?.parentElement?.removeChild(info.wrapperElement); + } else if (!info.settings.outlet) { + // Unwrap: move element back to wrapper's parent position, then remove wrapper + if (info.wrapperElement?.parentElement) { + info.wrapperElement.parentElement.insertBefore(child, info.wrapperElement); + info.wrapperElement.parentElement.removeChild(info.wrapperElement); + } + } else { + const outlet = info.settings.outlet.nativeElement || info.settings.outlet; + // if same element is shown in other overlay outlet will not contain + // the element and we should not remove it form outlet + if (outlet.contains(child)) { + outlet.removeChild(child.parentNode.parentNode); + } } if (info.componentRef) { this._appRef.detachView(info.componentRef.hostView); @@ -754,12 +778,7 @@ export class IgxOverlayService implements OnDestroy { const index = this._overlayInfos.indexOf(info); this._overlayInfos.splice(index, 1); - // this._overlayElement.parentElement check just for tests that manually delete the element if (this._overlayInfos.length === 0) { - if (this._overlayElement && this._overlayElement.parentElement) { - this._overlayElement.parentElement.removeChild(this._overlayElement); - this._overlayElement = null; - } this.removeCloseOnEscapeListener(); } diff --git a/projects/igniteui-angular/core/src/services/overlay/position/README.md b/projects/igniteui-angular/core/src/services/overlay/position/README.md index 1e4cb4d7367..f9339a13c02 100644 --- a/projects/igniteui-angular/core/src/services/overlay/position/README.md +++ b/projects/igniteui-angular/core/src/services/overlay/position/README.md @@ -1,20 +1,20 @@ # Position strategies Position strategies determine where to display the component in the provided IgxOverlayService. There are three position strategies: -1) **Global** - Positions the element based on the directions passed in trough PositionSettings. These are Top/Middle/Bottom for verticalDirection and Left/Center/Right for horizontalDirection. Defaults to: +1) **Global** - Positions the element based on the directions passed in through PositionSettings. These are Top/Middle/Bottom for verticalDirection and Left/Center/Right for horizontalDirection. Defaults to: | horizontalDirection | verticalDirection | |:---------------------------|:-------------------------| | HorizontalAlignment.Center | VerticalAlignment.Middle | -2) **Container** - Positions the element inside the containing outlet based on the directions passed in trough PositionSettings. These are Top/Middle/Bottom for verticalDirection and Left/Center/Right for horizontalDirection. Defaults to: +2) **Container** - Positions the element inside the containing outlet based on the directions passed in through PositionSettings. These are Top/Middle/Bottom for verticalDirection and Left/Center/Right for horizontalDirection. **Note:** The `outlet` property in `OverlaySettings` is still supported and used by the overlay service when provided but will be removed in a future version. Avoid using this property in new code and prefer the default in-place rendering with the HTML Popover API. Defaults to: | horizontalDirection | verticalDirection | |:---------------------------|:-------------------------| | HorizontalAlignment.Center | VerticalAlignment.Middle | -3) **Connected** - Positions the element based on the directions and start point passed in trough PositionSettings. It is possible to either pass a start point or an HTMLElement as a positioning base. Defaults to: +3) **Connected** - Positions the element based on the directions and start point passed in through PositionSettings. It is possible to either pass a start point or an HTMLElement as a positioning base. Defaults to: | target | horizontalDirection | verticalDirection | horizontalStartPoint | verticalStartPoint | |:----------------|:--------------------------|:-------------------------|:-------------------------|:-------------------------| @@ -64,7 +64,7 @@ import {AutoPositionStrategy, GlobalPositionStrategy, ConnectedPositioningStrate | Position Strategy | Name | Description | |:------------------|:-------------------------------------------------------|:----------------------------------------------------------------------------------| | Global | `position(contentElement)` | Positions the element, based on the horizontal and vertical directions. | -| Container | `position(contentElement)` | Positions the element inside the containing outlet based on the directions passed in trough PositionSettings. | +| Container | `position(contentElement)` | Positions the element inside the containing outlet based on the directions passed in through PositionSettings. | | Connected | `position(contentElement, size{})` | Positions the element, based on the position strategy used and the size passed in.| | Auto | `position(contentElement, size{}, document?)` | Positions the element, based on the position strategy used and the size passed in.| | Elastic | `position(contentElement, size{}, document?, minSize?)`| Positions the element, based on the position strategy used and the size passed in.| diff --git a/projects/igniteui-angular/core/src/services/overlay/utilities.ts b/projects/igniteui-angular/core/src/services/overlay/utilities.ts index 0cf7f38cdf0..1af3af78601 100644 --- a/projects/igniteui-angular/core/src/services/overlay/utilities.ts +++ b/projects/igniteui-angular/core/src/services/overlay/utilities.ts @@ -131,7 +131,11 @@ export interface OverlaySettings { /** Set if the overlay should close when `Esc` key is pressed */ closeOnEscape?: boolean; /* blazorSuppress */ - /** Set the outlet container to attach the overlay to */ + /** + * @deprecated Still supported and used by the overlay service when provided but will + * be removed in a future version. Avoid using this property in new code and prefer + * the default in-place rendering with the HTML Popover API. + */ outlet?: IgxOverlayOutletDirective | ElementRef; /** * @hidden @internal @@ -200,7 +204,8 @@ export interface OverlayInfo { transformY?: number; event?: Event; wrapperElement?: HTMLElement; - size?: string + size?: string; + appendedToBody?: boolean } /** @hidden */ 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 71c402fb224..0591c25ee10 100644 --- a/projects/igniteui-angular/date-picker/src/date-picker/README.md +++ b/projects/igniteui-angular/date-picker/src/date-picker/README.md @@ -113,7 +113,7 @@ The date picker also supports binding through `ngModel` if two-way date-bind is | `overlaySettings` | Changes the default overlay settings used by the `IgxDatePickerComponent`. | OverlaySettings | | `placeholder` | Sets the placeholder text for empty input. | string | | `disabled` | Disables or enables the picker. | boolean | -| `outlet` | The container used for the pop up element. | IgxOverlayOutletDirective \| ElementRef | +| `outlet` | **Deprecated.** Still honored by the overlay service for now, but will be removed in a future version. Prefer configuring a custom outlet via `overlaySettings.outlet`. | IgxOverlayOutletDirective \| ElementRef | | `type` | Determines how the picker will be styled. | IgxInputGroupType | | `spinLoop` | Determines if the currently spun date segment should loop over. | boolean | | `spinDelta` | Delta values used to increment or decrement each editor date part on spin actions. All values default to `1`. | DatePartDeltas | diff --git a/projects/igniteui-angular/date-picker/src/date-picker/calendar-container/calendar-container.component.ts b/projects/igniteui-angular/date-picker/src/date-picker/calendar-container/calendar-container.component.ts index a4a904676cc..a2aded5b439 100644 --- a/projects/igniteui-angular/date-picker/src/date-picker/calendar-container/calendar-container.component.ts +++ b/projects/igniteui-angular/date-picker/src/date-picker/calendar-container/calendar-container.component.ts @@ -64,6 +64,7 @@ export class IgxCalendarContainerComponent { @HostListener('keydown.alt.arrowup', ['$event']) public onEscape(event) { event.preventDefault(); + event.stopPropagation(); this.calendarClose.emit(); } 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 b13f9a0beb2..39453266b30 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 @@ -244,6 +244,10 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr public spinDelta: Pick; /** + * @deprecated Still supported and used by the overlay service when provided but will + * be removed in a future version. Avoid using this property in new code and prefer + * the default in-place rendering with the HTML Popover API. + * * Gets/Sets the container used for the popup element. * * @remarks diff --git a/projects/igniteui-angular/date-picker/src/date-picker/picker-base.directive.ts b/projects/igniteui-angular/date-picker/src/date-picker/picker-base.directive.ts index d9d005be4fb..b1d637d6bbf 100644 --- a/projects/igniteui-angular/date-picker/src/date-picker/picker-base.directive.ts +++ b/projects/igniteui-angular/date-picker/src/date-picker/picker-base.directive.ts @@ -186,6 +186,10 @@ export abstract class PickerBaseDirective implements IToggleView, EditorProvider } /** + * @deprecated Still supported and used by the overlay service when provided but will + * be removed in a future version. Avoid using this property in new code and prefer + * the default in-place rendering with the HTML Popover API. + * * The container used for the pop-up element. * * @example diff --git a/projects/igniteui-angular/date-picker/src/date-range-picker/README.md b/projects/igniteui-angular/date-picker/src/date-range-picker/README.md index ca7d824b83d..6c6e7f461f7 100644 --- a/projects/igniteui-angular/date-picker/src/date-range-picker/README.md +++ b/projects/igniteui-angular/date-picker/src/date-range-picker/README.md @@ -116,7 +116,7 @@ With projected inputs: | mode | PickerInteractionMode | Sets whether `IgxDateRangePickerComponent` is in dialog or dropdown mode. Default is `dialog` | | minValue | Date \| string | The minimum value in a valid range. | | maxValue | Date \| string | The maximum value in a valid range. | -| outlet | IgxOverlayOutletDirective \| ElementRef | Gets/Sets the container used for the popup element. +| outlet | IgxOverlayOutletDirective \| ElementRef | **Deprecated and will be removed in a future version.** The overlay service currently still honors this setting, but it may stop working in a future release. | overlaySettings | OverlaySettings | Changes the default overlay settings used by the `IgxDateRangePickerComponent`. | | placeholder | string | Sets the `placeholder` for single-input `IgxDateRangePickerComponent`. | | weekStart | number | Sets the start day of the week. Can be assigned to a numeric value or to `WEEKDAYS` enum value. | 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 ae6cd0cee55..b75e6725c08 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 @@ -786,7 +786,7 @@ describe('IgxDateRangePicker', () => { .toBeTrue(); expect(dateRange.isFocused).toBeTrue(); - UIInteractions.triggerKeyDownEvtUponElem('ArrowUp', calendarWrapper, true, true); + UIInteractions.triggerEventHandlerKeyDown('ArrowUp', range, true); tick(); fixture.detectChanges(); expect(dateRange.collapsed).toBeTruthy(); 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 049d44ce91a..8d40b7a18d6 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 @@ -111,6 +111,7 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective private _injector = inject(Injector); private _cdr = inject(ChangeDetectorRef); private _overlayService = inject(IgxOverlayService); + private _hostViewContainerRef = inject(ViewContainerRef); /** @@ -366,6 +367,10 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective public override placeholder = ''; /** + * @deprecated Still supported and used by the overlay service when provided but will + * be removed in a future version. Avoid using this property in new code and prefer + * the default in-place rendering with the HTML Popover API. + * * Gets/Sets the container used for the popup element. * * @remarks @@ -436,7 +441,11 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective public cssClass = 'igx-date-range-picker'; @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef }) - private viewContainerRef: ViewContainerRef; + private _inputGroupViewContainerRef: ViewContainerRef; + + private get viewContainerRef(): ViewContainerRef { + return this._inputGroupViewContainerRef ?? this._hostViewContainerRef; + } /** @hidden @internal */ @ViewChild(IgxInputDirective) 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 fea22acc8d9..ecda1d42886 100644 --- a/projects/igniteui-angular/dialog/src/dialog/dialog.component.spec.ts +++ b/projects/igniteui-angular/dialog/src/dialog/dialog.component.spec.ts @@ -390,8 +390,7 @@ describe('Dialog', () => { tick(); fix.detectChanges(); - let overlaydiv = document.getElementsByClassName(OVERLAY_MAIN_CLASS)[0]; - let overlayWrapper = overlaydiv.children[0]; + let overlayWrapper = (dialog as any).elementRef.nativeElement.children[0]; expect(overlayWrapper.classList.contains(OVERLAY_WRAPPER_CLASS)).toBe(true); expect(overlayWrapper.classList.contains(OVERLAY_MODAL_WRAPPER_CLASS)).toBe(false); @@ -406,8 +405,7 @@ describe('Dialog', () => { tick(16); fix.detectChanges(); - overlaydiv = document.getElementsByClassName(OVERLAY_MAIN_CLASS)[0]; - overlayWrapper = overlaydiv.children[0]; + overlayWrapper = (dialog as any).elementRef.nativeElement.children[0]; expect(overlayWrapper.classList.contains(OVERLAY_MODAL_WRAPPER_CLASS)).toBe(true); expect(overlayWrapper.classList.contains(OVERLAY_WRAPPER_CLASS)).toBe(true); })); @@ -418,7 +416,6 @@ describe('Dialog', () => { const dialog: IgxDialogComponent = fix.componentInstance.dialog as IgxDialogComponent; dialog.open(); - tick(100); fix.detectChanges(); tick(100); diff --git a/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts b/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts index 0df270be7f5..54a2739c8c3 100644 --- a/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts @@ -30,6 +30,10 @@ export abstract class IgxNotificationsDirective extends IgxToggleDirective public displayTime = 4000; /** + * @deprecated The `outlet` property is deprecated and will be removed in a future version. + * While still honored by the overlay service, it should no longer be used in new code. + * Future versions will always render overlay content in place using the HTML Popover API. + * * Gets/Sets the container used for the element. * * @remarks diff --git a/projects/igniteui-angular/directives/src/directives/toggle/README.md b/projects/igniteui-angular/directives/src/directives/toggle/README.md index c2529f6605e..86f287dd61a 100644 --- a/projects/igniteui-angular/directives/src/directives/toggle/README.md +++ b/projects/igniteui-angular/directives/src/directives/toggle/README.md @@ -103,10 +103,12 @@ Providing reference from custom component which has already been registered into |:----------:|:-------------|:------| | `igxToggleAction`| `IToggleView` \| `string` | Determines the target that have to be controled. | | `overlaySettings` | `OverlaySettings`| Passes `igxOverlay` settings for applicable targets (`igxToggle`) that control positioning, interaction and scroll behavior. -| `igxToggleOutlet` | `IgxOverlayOutletDirective` \| `ElementRef`| Determines where the target overlay element should be attached. Shortcut for `overlaySettings.outlet`. +| `igxToggleOutlet` | `IgxOverlayOutletDirective` \| `ElementRef`| **Deprecated.** Still supported and used by the overlay service when provided but will be removed in a future version. Avoid using this property in new code and prefer the default in-place rendering with the HTML Popover API. # IgxOverlayOutlet Directive +> **Deprecated.** The `IgxOverlayOutletDirective` and the `outlet` property on `OverlaySettings` are deprecated. They are still supported and used by the overlay service when provided but will be removed in a future version. Avoid using this property in new code and prefer the default in-place rendering with the HTML Popover API. + The **IgxOverlayOutlet** provides a way to mark an element as an `igxOverlay` outlet container through the component template only. Directive instance is exported as `overlay-outlet`, so it can be assigned within the template: @@ -118,3 +120,5 @@ This allows to provide the `outlet` templates variable as a setting to the toggl ``` + +The overlay service now always renders content in place using the HTML Popover API, so specifying an outlet is no longer necessary. Simply remove the `outlet` and `igxToggleOutlet` references from your templates and settings. diff --git a/projects/igniteui-angular/directives/src/directives/toggle/toggle.directive.ts b/projects/igniteui-angular/directives/src/directives/toggle/toggle.directive.ts index c4712fbdc78..2aea1c03a3f 100644 --- a/projects/igniteui-angular/directives/src/directives/toggle/toggle.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/toggle/toggle.directive.ts @@ -444,6 +444,10 @@ export class IgxToggleActionDirective implements OnInit { *
* ``` * Where `outlet` in an instance of `IgxOverlayOutletDirective` or an `ElementRef` + * + * @deprecated Still supported and used by the overlay service when provided but will + * be removed in a future version. Avoid using this property in new code and prefer + * the default in-place rendering with the HTML Popover API. */ @Input('igxToggleOutlet') public outlet: IgxOverlayOutletDirective | ElementRef; diff --git a/projects/igniteui-angular/drop-down/src/drop-down/autocomplete/README.md b/projects/igniteui-angular/drop-down/src/drop-down/autocomplete/README.md index e3edcc21f01..1f98489dc62 100644 --- a/projects/igniteui-angular/drop-down/src/drop-down/autocomplete/README.md +++ b/projects/igniteui-angular/drop-down/src/drop-down/autocomplete/README.md @@ -97,7 +97,9 @@ export class AutocompleteSampleComponent { > Note: When autocomplete is dynamically disabled, then it will be automatically closed. ### Drop Down settings -The igx-autocomplete drop down positioning, scrolling strategy and outlet can be configured using, the `igxAutocompleteSettings` option. It allows values from type `AutocompleteOverlaySettings`. +The igx-autocomplete drop down positioning and scrolling strategy can be configured using the `igxAutocompleteSettings` option. It allows values from type `AutocompleteOverlaySettings`. + +> **Note:** The `outlet` property in overlay settings is deprecated and will be removed in a future version. While it is still honored by the overlay service today, new code should avoid relying on it and prefer the default in-place rendering behavior (HTML Popover API). The following example displays that the positioning of the drop down can be set to be always above the input, where the directive is applied. It also disables opening and closing animations. For that purpose the `ConnectedPositioningStrategy` is used: diff --git a/projects/igniteui-angular/drop-down/src/drop-down/autocomplete/autocomplete.directive.ts b/projects/igniteui-angular/drop-down/src/drop-down/autocomplete/autocomplete.directive.ts index 00f8ba12d21..726fb5a9f83 100644 --- a/projects/igniteui-angular/drop-down/src/drop-down/autocomplete/autocomplete.directive.ts +++ b/projects/igniteui-angular/drop-down/src/drop-down/autocomplete/autocomplete.directive.ts @@ -32,7 +32,12 @@ export interface AutocompleteOverlaySettings { positionStrategy?: IPositionStrategy; /** Scroll strategy to use with this settings */ scrollStrategy?: IScrollStrategy; - /** Set the outlet container to attach the overlay to */ + /** + * @deprecated Still supported and used by the overlay service when provided but will + * be removed in a future version. Avoid using this property in new code and prefer + * the default in-place rendering with the HTML Popover API. + * + * Set the outlet container to attach the overlay to */ outlet?: IgxOverlayOutletDirective | ElementRef; } diff --git a/projects/igniteui-angular/grids/core/src/common/grid.interface.ts b/projects/igniteui-angular/grids/core/src/common/grid.interface.ts index 9a5ef823279..f95499114d3 100644 --- a/projects/igniteui-angular/grids/core/src/common/grid.interface.ts +++ b/projects/igniteui-angular/grids/core/src/common/grid.interface.ts @@ -518,6 +518,11 @@ export interface GridType extends IGridDataBindable { navigation: any; /** @hidden @internal */ filteringService: any; + /** + * @deprecated The `outlet` property is deprecated and will be removed in a future version. + * It is still honored by the overlay service to determine where the overlay wrapper is attached, + * but no new code should rely on it and existing usages should migrate away from it. + */ outlet: any; /** Indicates whether the grid has columns that can be moved */ /** @hidden @internal */ diff --git a/projects/igniteui-angular/grids/core/src/filtering/excel-style/excel-style-default-expression.component.ts b/projects/igniteui-angular/grids/core/src/filtering/excel-style/excel-style-default-expression.component.ts index ae41bad424b..39af1074e70 100644 --- a/projects/igniteui-angular/grids/core/src/filtering/excel-style/excel-style-default-expression.component.ts +++ b/projects/igniteui-angular/grids/core/src/filtering/excel-style/excel-style-default-expression.component.ts @@ -46,6 +46,11 @@ export class IgxExcelStyleDefaultExpressionComponent implements AfterViewInit { @Output() public logicOperatorChanged = new EventEmitter(); + /** + * @deprecated The `outlet` property is deprecated but still functional until its removal in a future version. + * It continues to be passed to the overlay service via `OverlaySettings.outlet`, but consumers should migrate away + * from it as it may be removed in a later release. + */ @ViewChild('overlayOutlet', { read: IgxOverlayOutletDirective, static: true }) public overlayOutlet: IgxOverlayOutletDirective; diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index b64e34ce402..56989f79b2e 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -1147,6 +1147,7 @@ export abstract class IgxGridBaseDirective implements GridType, @ViewChild('loadingOverlay', { read: IgxToggleDirective, static: true }) public loadingOverlay: IgxToggleDirective; + // outlet - not public - do not deprecate /** * @hidden @internal */ @@ -1335,6 +1336,7 @@ export abstract class IgxGridBaseDirective implements GridType, @ViewChild('tfoot', { static: true }) public tfoot: ElementRef; + // outlet - not public - do not deprecate /** * @hidden @internal */ @@ -1760,6 +1762,7 @@ export abstract class IgxGridBaseDirective implements GridType, @ContentChildren(IgxPaginatorToken) protected paginationComponents: QueryList; + // outlet - not public - do not deprecate /** * @hidden @internal */ @@ -4487,6 +4490,9 @@ export abstract class IgxGridBaseDirective implements GridType, } /** + * @deprecated The `outlet` property is deprecated and will be removed in a future version. + * It is still honored by the overlay service for attaching the grid's overlays to a specific outlet. + * * Gets/Sets the outlet used to attach the grid's overlays to. * * @remarks diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid-base.directive.ts b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid-base.directive.ts index 5acc69f1fa6..ce825c26f40 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid-base.directive.ts +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid-base.directive.ts @@ -83,6 +83,10 @@ export abstract class IgxHierarchicalGridBaseDirective extends IgxGridBaseDirect /* blazorSuppress */ /** + * @deprecated Still supported and used by the overlay service when provided but will + * be removed in a future version. Avoid using this property in new code and prefer + * the default in-place rendering with the HTML Popover API. + * * Gets the outlet used to attach the grid's overlays to. * * @remarks @@ -94,6 +98,10 @@ export abstract class IgxHierarchicalGridBaseDirective extends IgxGridBaseDirect /* blazorSuppress */ /** + * @deprecated Still supported and used by the overlay service when provided but will + * be removed in a future version. Avoid using this property in new code and prefer + * the default in-place rendering with the HTML Popover API. + * * Sets the outlet used to attach the grid's overlays to. */ public override set outlet(val: any) { diff --git a/projects/igniteui-angular/grids/pivot-grid/src/pivot-grid.component.ts b/projects/igniteui-angular/grids/pivot-grid/src/pivot-grid.component.ts index afc3939de51..2c94e7184be 100644 --- a/projects/igniteui-angular/grids/pivot-grid/src/pivot-grid.component.ts +++ b/projects/igniteui-angular/grids/pivot-grid/src/pivot-grid.component.ts @@ -1079,6 +1079,7 @@ export class IgxPivotGridComponent extends IgxGridBaseDirective implements OnIni /** @hidden @internal */ public createFilterESF(dropdown: any, column: ColumnType, options: OverlaySettings, shouldReatach: boolean) { options.outlet = this.outlet; + if (dropdown) { dropdown.initialize(column, this.overlayService); dropdown.populateData(); diff --git a/projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.ts b/projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.ts index 729b6ce9bb9..c90d031de6a 100644 --- a/projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.ts +++ b/projects/igniteui-angular/query-builder/src/query-builder/query-builder-tree.component.ts @@ -360,6 +360,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { @ViewChild('expressionsContainer') private expressionsContainer: ElementRef; + // outlet - not public - do not deprecate @ViewChild('overlayOutlet', { read: IgxOverlayOutletDirective, static: true }) private overlayOutlet: IgxOverlayOutletDirective; @@ -406,6 +407,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { */ public searchValue: { value: any } = { value: null }; + // outlet - not public - do not deprecate /** * @hidden @internal */ @@ -741,6 +743,7 @@ export class IgxQueryBuilderTreeComponent implements AfterViewInit, OnDestroy { return this._selectedField; } + // outlet - not public - do not deprecate /** * @hidden @internal * diff --git a/projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.ts b/projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.ts index 37e9aa1a898..762408b85fe 100644 --- a/projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.ts +++ b/projects/igniteui-angular/query-builder/src/query-builder/query-builder.component.ts @@ -291,6 +291,7 @@ export class IgxQueryBuilderComponent implements OnDestroy { this.destroy$.complete(); } + // outlet - not public - do not deprecate /** * @hidden @internal * diff --git a/projects/igniteui-angular/test-utils/ui-interactions.spec.ts b/projects/igniteui-angular/test-utils/ui-interactions.spec.ts index 3bd6c261053..d792aef5cad 100644 --- a/projects/igniteui-angular/test-utils/ui-interactions.spec.ts +++ b/projects/igniteui-angular/test-utils/ui-interactions.spec.ts @@ -28,8 +28,8 @@ export class UIInteractions { * Clears all opened overlays and resets document scrollTop and scrollLeft */ public static clearOverlay() { - const overlays = document.getElementsByClassName('igx-overlay') as HTMLCollectionOf; - Array.from(overlays).forEach(element => { + const overlays = document.querySelectorAll('[class*="igx-overlay__wrapper"]'); + overlays.forEach(element => { element.remove(); }); document.documentElement.scrollTop = 0; diff --git a/src/app/overlay/overlay.sample.ts b/src/app/overlay/overlay.sample.ts index 7029549ad1a..a582d666eb9 100644 --- a/src/app/overlay/overlay.sample.ts +++ b/src/app/overlay/overlay.sample.ts @@ -225,7 +225,7 @@ export class OverlaySampleComponent implements OnInit { positionStrategy: stringMapping['PositionStrategy'][this.positionStrategy], scrollStrategy: stringMapping['ScrollStrategy'][this.scrollStrategy], modal: this.modal, - closeOnOutsideClick: this.closeOnOutsideClick + closeOnOutsideClick: this.closeOnOutsideClick, }; this._overlaySettings.positionStrategy.settings.verticalDirection = stringMapping['VerticalDirection'][this.verticalDirection]; From 91d3085fb295fd7a55f4d8e379b70acab7a51ebf Mon Sep 17 00:00:00 2001 From: wnvko Date: Fri, 13 Mar 2026 12:54:23 +0200 Subject: [PATCH 2/3] feat(overlay): remove outlet from container position strategy --- CHANGELOG.md | 7 ++ .../core/src/services/overlay/README.md | 6 +- .../core/src/services/overlay/overlay.spec.ts | 117 +++++++++--------- .../core/src/services/overlay/overlay.ts | 34 ++++- .../src/services/overlay/position/README.md | 31 ++++- .../position/container-position-strategy.ts | 44 ++++--- .../position/global-position-strategy.ts | 4 +- .../notification/notifications.directive.ts | 14 ++- .../grids/grid/src/grid-base.directive.ts | 6 +- .../grids/grid/src/grid.component.html | 7 +- .../src/hierarchical-grid.component.html | 8 +- .../pivot-grid/src/pivot-grid.component.html | 6 +- .../tree-grid/src/tree-grid.component.html | 8 +- projects/igniteui-angular/snackbar/README.md | 19 +++ .../src/snackbar/snackbar.component.ts | 5 +- projects/igniteui-angular/toast/README.md | 18 ++- src/app/overlay/overlay-presets.sample.html | 6 +- src/app/overlay/overlay-presets.sample.scss | 2 +- src/app/overlay/overlay-presets.sample.ts | 6 +- src/app/overlay/overlay.sample.html | 2 +- src/app/overlay/overlay.sample.ts | 15 ++- 21 files changed, 235 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 804e1c0aad7..22086973431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ All notable changes for each version of this project will be documented in this - The overlay service now integrates the HTML Popover API and prefers rendering content in place / in the top layer, significantly reducing the need for outlet containers. When configured, `OverlaySettings.outlet` is still honored, and overlays may also fall back to being appended to `document.body` when required. - **Deprecation** - The `outlet` property in `OverlaySettings`, `IgxOverlayOutletDirective`, and `igxToggleOutlet` input on `IgxToggleActionDirective` are deprecated and will be removed in a future version. They remain functional in this release for backward compatibility, but new code should rely on the default in-place / top-layer rendering behavior instead of custom outlet containers. + + - `ContainerPositionStrategy` - The `ContainerPositionStrategy` now uses the `target` property from `OverlaySettings` (when set to an `HTMLElement`) as the container in which the overlay is rendered. This replaces the previous reliance on the deprecated `outlet` property and internal DOM traversal. The overlay wrapper is sized and positioned to match the target container's bounds and automatically updates on resize via `ResizeObserver`. + + - `IgxOverlayService.createAbsoluteOverlaySettings` - Added a new overload accepting `container?: HTMLElement` as the second parameter. When a container element is provided, a `ContainerPositionStrategy` is used and the `target` in the returned overlay settings is set to the container element. The previous overload accepting `outlet?: IgxOverlayOutletDirective | ElementRef` is still supported but deprecated. + +- `IgxNotificationsDirective`, `IgxSnackbarComponent`, `IgxToastComponent` + - Added a new `container` input property of type `HTMLElement`. When set, overlay content is rendered inside the given container using `ContainerPositionStrategy`. The deprecated `outlet` property now points users to `container` as its replacement. ## 21.1.0 ### New Features diff --git a/projects/igniteui-angular/core/src/services/overlay/README.md b/projects/igniteui-angular/core/src/services/overlay/README.md index d47423fd8db..bbbd02cb02c 100644 --- a/projects/igniteui-angular/core/src/services/overlay/README.md +++ b/projects/igniteui-angular/core/src/services/overlay/README.md @@ -73,12 +73,12 @@ this.overlay.show(component, overlaySettings); | Name | Type | Description | | :--- | :--- | :---------- | -| target | Point | HTMLElement | Attaching target for the component to show | +| target | Point | HTMLElement | Attaching target for the component to show. When set to an `HTMLElement` in combination with `ContainerPositionStrategy`, the element serves as the container in which the overlay is rendered. | | positionStrategy | IPositionStrategy | Position strategy to use with this settings | | scrollStrategy | IScrollStrategy | Scroll strategy to use with this settings | | modal | boolean | Set if the overlay should be in modal mode | | closeOnOutsideClick | boolean | Set if the overlay should closed on outside click | -| outlet | IgxOverlayOutletDirective or ElementRef | **Deprecated.** Still supported and used by the overlay service when provided but will be removed in a future version. Avoid using this property in new code and prefer the default in-place rendering with the HTML Popover API. | +| outlet | IgxOverlayOutletDirective or ElementRef | **Deprecated.** Use `target` with an `HTMLElement` and `ContainerPositionStrategy` instead. Still supported for backward compatibility but will be removed in a future version. | ###### PositionSettings @@ -135,7 +135,7 @@ this.overlay.show(component, overlaySettings); | Name | Description | Parameters | |-----------------|---------------------------------------------------------------------------------|------------| |getPointFromPositionsSettings| Calculates the point from which the overlay should start showing |settings | -|createAbsoluteOverlaySettings| Creates overlay settings with a global or container position strategy based on preset position settings and the provided outlet. When an outlet is specified, a container strategy is used and the content is rendered inside that outlet; otherwise, a global strategy is used and the content is rendered in place. |position?, outlet?| +|createAbsoluteOverlaySettings| Creates overlay settings with a global or container position strategy based on preset position settings. Accepts either a deprecated outlet (`IgxOverlayOutletDirective` / `ElementRef`) or an `HTMLElement` container. When a container or outlet is provided, a `ContainerPositionStrategy` is used; otherwise, a `GlobalPositionStrategy` is used. |position?, container?| |createRelativeOverlaySettings| Creates overlay settings with auto, connected or elastic position strategy based on a preset position settings |target, strategy?, position?| 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 0685394a36f..a97bb2f03e9 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.spec.ts @@ -3414,41 +3414,41 @@ describe('igxOverlay', () => { })); // 1.5 GlobalContainer. - it('Should center the shown component in the outlet.', fakeAsync(() => { + it('Should center the shown component in the container.', fakeAsync(() => { const fixture = TestBed.createComponent(EmptyPageComponent); - const outlet = fixture.componentInstance.divElement; - const outletElement = outlet.nativeElement; - outletElement.style.width = '800px'; - outletElement.style.height = '600px'; - outletElement.style.position = 'fixed'; - outletElement.style.top = '100px'; - outletElement.style.left = '200px'; - outletElement.style.overflow = 'hidden'; + const container = fixture.componentInstance.divElement; + const containerElement = container.nativeElement; + containerElement.style.width = '800px'; + containerElement.style.height = '600px'; + containerElement.style.position = 'fixed'; + containerElement.style.top = '100px'; + containerElement.style.left = '200px'; + containerElement.style.overflow = 'hidden'; fixture.detectChanges(); const overlaySettings: OverlaySettings = { - outlet, + target: containerElement, positionStrategy: new ContainerPositionStrategy() }; - const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, overlaySettings); + const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, overlaySettings); fixture.componentInstance.overlay.show(id); tick(); - const overlayElement = outletElement.children[0]; + const overlayElement = containerElement.children[0]; const overlayElementRect = overlayElement.getBoundingClientRect(); - expect(overlayElementRect.width).toBeCloseTo(800, 1); - expect(overlayElementRect.height).toBeCloseTo(600, 1); + expect(overlayElementRect.width).toBeCloseTo(800, 0); + expect(overlayElementRect.height).toBeCloseTo(600, 0); const wrapperElement = overlayElement.children[0] as HTMLElement; const componentElement = wrapperElement.children[0].children[0]; const componentRect = componentElement.getBoundingClientRect(); - const outletRect = outletElement.getBoundingClientRect(); + const containerRect = containerElement.getBoundingClientRect(); - // Check component is centered relative to outlet - const horizontalCenter = Math.abs((componentRect.left + componentRect.width / 2) - (outletRect.left + outletRect.width / 2)); - const verticalCenter = Math.abs((componentRect.top + componentRect.height / 2) - (outletRect.top + outletRect.height / 2)); + // Check component is centered relative to container + const horizontalCenter = Math.abs((componentRect.left + componentRect.width / 2) - (containerRect.left + containerRect.width / 2)); + const verticalCenter = Math.abs((componentRect.top + componentRect.height / 2) - (containerRect.top + containerRect.height / 2)); expect(horizontalCenter).toBeLessThan(1); expect(verticalCenter).toBeLessThan(1); @@ -3458,27 +3458,27 @@ describe('igxOverlay', () => { it('Should reposition overlay when outlet is resized with ContainerPositionStrategy.', async() => { const fixture = TestBed.createComponent(EmptyPageComponent); - const outlet = fixture.componentInstance.divElement; - const outletElement = outlet.nativeElement; - outletElement.style.width = '800px'; - outletElement.style.height = '600px'; - outletElement.style.position = 'fixed'; - outletElement.style.top = '100px'; - outletElement.style.left = '200px'; - outletElement.style.overflow = 'hidden'; + const container = fixture.componentInstance.divElement; + const containerElement = container.nativeElement; + containerElement.style.width = '800px'; + containerElement.style.height = '600px'; + containerElement.style.position = 'fixed'; + containerElement.style.top = '100px'; + containerElement.style.left = '200px'; + containerElement.style.overflow = 'hidden'; fixture.detectChanges(); const overlaySettings: OverlaySettings = { - outlet, + target: containerElement, positionStrategy: new ContainerPositionStrategy() }; - const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, overlaySettings); + const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, overlaySettings); fixture.componentInstance.overlay.show(id); fixture.detectChanges(); await wait(100); - let wrapperElement = outletElement.children[0]; + let wrapperElement = containerElement.children[0]; let wrapperRect = wrapperElement.getBoundingClientRect(); // Initial wrapper dimensions should match outlet @@ -3486,13 +3486,13 @@ describe('igxOverlay', () => { expect(wrapperRect.height).toBeCloseTo(600, 0); // Resize the outlet - outletElement.style.width = '1000px'; - outletElement.style.height = '700px'; + containerElement.style.width = '1000px'; + containerElement.style.height = '700px'; fixture.detectChanges(); await wait(100); // Wrapper should now match new outlet dimensions - wrapperElement = outletElement.children[0]; + wrapperElement = containerElement.children[0]; wrapperRect = wrapperElement.getBoundingClientRect(); expect(wrapperRect.width).toBeCloseTo(1000, 0); expect(wrapperRect.height).toBeCloseTo(700, 0); @@ -3503,30 +3503,30 @@ describe('igxOverlay', () => { it('Should maintain centering when outlet is resized with ContainerPositionStrategy.', async () => { const fixture = TestBed.createComponent(EmptyPageComponent); - const outlet = fixture.componentInstance.divElement; - const outletElement = outlet.nativeElement; - outletElement.style.width = '600px'; - outletElement.style.height = '400px'; - outletElement.style.position = 'fixed'; - outletElement.style.top = '50px'; - outletElement.style.left = '50px'; - outletElement.style.overflow = 'hidden'; + const container = fixture.componentInstance.divElement; + const containerElement = container.nativeElement; + containerElement.style.width = '600px'; + containerElement.style.height = '400px'; + containerElement.style.position = 'fixed'; + containerElement.style.top = '50px'; + containerElement.style.left = '50px'; + containerElement.style.overflow = 'hidden'; fixture.detectChanges(); const overlaySettings: OverlaySettings = { - outlet, + target: containerElement, positionStrategy: new ContainerPositionStrategy() }; - const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, overlaySettings); + const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, fixture.componentInstance.viewContainerRef, overlaySettings); fixture.componentInstance.overlay.show(id); fixture.detectChanges(); await wait(100); - const wrapperElement = outletElement.children[0]; + const wrapperElement = containerElement.children[0]; let componentElement = wrapperElement.children[0].children[0]; let componentRect = componentElement.getBoundingClientRect(); - let outletRect = outletElement.getBoundingClientRect(); + let outletRect = containerElement.getBoundingClientRect(); // Verify initial centering let horizontalCenter = Math.abs((componentRect.left + componentRect.width / 2) - (outletRect.left + outletRect.width / 2)); @@ -3535,8 +3535,8 @@ describe('igxOverlay', () => { expect(verticalCenter).toBeLessThan(2); // Resize the outlet - outletElement.style.width = '900px'; - outletElement.style.height = '600px'; + containerElement.style.width = '900px'; + containerElement.style.height = '600px'; // Wait for ResizeObserver to fire for size change await wait(100); @@ -3544,12 +3544,12 @@ describe('igxOverlay', () => { // Re-check centering with new dimensions componentElement = wrapperElement.children[0].children[0]; componentRect = componentElement.getBoundingClientRect(); - outletRect = outletElement.getBoundingClientRect(); + outletRect = containerElement.getBoundingClientRect(); horizontalCenter = Math.abs((componentRect.left + componentRect.width / 2) - (outletRect.left + outletRect.width / 2)); verticalCenter = Math.abs((componentRect.top + componentRect.height / 2) - (outletRect.top + outletRect.height / 2)); - expect(horizontalCenter).toBeLessThan(2); - expect(verticalCenter).toBeLessThan(2); + expect(horizontalCenter).toBeLessThan(1); + expect(verticalCenter).toBeLessThan(1); fixture.componentInstance.overlay.detachAll(); }); @@ -3557,19 +3557,19 @@ describe('igxOverlay', () => { it('Should dispose IntersectionObserver when overlay is detached.', async () => { const fixture = TestBed.createComponent(EmptyPageComponent); - const outlet = fixture.componentInstance.divElement; - const outletElement = outlet.nativeElement; - outletElement.style.width = '800px'; - outletElement.style.height = '600px'; - outletElement.style.position = 'fixed'; - outletElement.style.top = '100px'; - outletElement.style.left = '200px'; + const container = fixture.componentInstance.divElement; + const containerElement = container.nativeElement; + containerElement.style.width = '800px'; + containerElement.style.height = '600px'; + containerElement.style.position = 'fixed'; + containerElement.style.top = '100px'; + containerElement.style.left = '200px'; fixture.detectChanges(); const positionStrategy = new ContainerPositionStrategy(); spyOn(positionStrategy, 'dispose').and.callThrough(); const overlaySettings: OverlaySettings = { - outlet, + target: containerElement, positionStrategy }; @@ -3577,7 +3577,7 @@ describe('igxOverlay', () => { fixture.componentInstance.overlay.show(id); await wait(100); - const wrapperElement = outletElement.children[0]; + const wrapperElement = containerElement.children[0]; const wrapperRect = wrapperElement.getBoundingClientRect(); // Initial dimensions @@ -4851,6 +4851,7 @@ describe('igxOverlay', () => { standalone: true }) export class SimpleDynamicComponent { + public viewContainerRef = inject(ViewContainerRef); @HostBinding('style.display') public hostDisplay = 'block'; @HostBinding('style.width') diff --git a/projects/igniteui-angular/core/src/services/overlay/overlay.ts b/projects/igniteui-angular/core/src/services/overlay/overlay.ts index 793db60c791..3be019ab7c7 100644 --- a/projects/igniteui-angular/core/src/services/overlay/overlay.ts +++ b/projects/igniteui-angular/core/src/services/overlay/overlay.ts @@ -137,6 +137,8 @@ export class IgxOverlayService implements OnDestroy { } /** + * @deprecated The outlet parameter is deprecated. Please provide the container or outlet element through the + * `createAbsoluteOverlaySettings` method when calling `attach` method. * Creates overlay settings with global or container position strategy and preset position settings * * @param position Preset position settings. Default position is 'center' @@ -144,15 +146,29 @@ export class IgxOverlayService implements OnDestroy { * @returns Non-modal overlay settings based on Global or Container position strategy and the provided position. */ public static createAbsoluteOverlaySettings( - position?: AbsolutePosition, outlet?: IgxOverlayOutletDirective | ElementRef): OverlaySettings { + position?: AbsolutePosition, outlet?: IgxOverlayOutletDirective | ElementRef): OverlaySettings; + /** + * Creates overlay settings with global or container position strategy and preset position settings + * + * @param position Preset position settings. Default position is 'center' + * @param container The container element to attach the overlay to + * @returns Non-modal overlay settings based on Global or Container position strategy and the provided position. + */ + public static createAbsoluteOverlaySettings( + position?: AbsolutePosition, container?: HTMLElement): OverlaySettings; + public static createAbsoluteOverlaySettings( + position?: AbsolutePosition, containerOrOutlet?: IgxOverlayOutletDirective | ElementRef | HTMLElement): OverlaySettings { const positionSettings = this.createAbsolutePositionSettings(position); - const strategy = outlet ? new ContainerPositionStrategy(positionSettings) : new GlobalPositionStrategy(positionSettings); + const strategy = containerOrOutlet ? new ContainerPositionStrategy(positionSettings) : new GlobalPositionStrategy(positionSettings); + const container = containerOrOutlet instanceof HTMLElement ? containerOrOutlet : null; + const outlet = containerOrOutlet instanceof ElementRef || containerOrOutlet instanceof IgxOverlayOutletDirective ? containerOrOutlet : null; const overlaySettings: OverlaySettings = { positionStrategy: strategy, scrollStrategy: new NoOpScrollStrategy(), modal: false, closeOnOutsideClick: true, - outlet + outlet, + target: container }; return overlaySettings; } @@ -333,7 +349,8 @@ export class IgxOverlayService implements OnDestroy { info.initialSize = { width: elementRect.width, height: elementRect.height }; // Get the size before moving the container into the overlay so that it does not forget about inherited styles. this.getComponentSize(info); - if (info.settings.outlet) { + if (info.settings.outlet || + (info.settings.positionStrategy instanceof ContainerPositionStrategy && info.settings.target instanceof HTMLElement)) { this.moveElementToOutlet(info); } else if (info.elementRef.nativeElement.parentElement) { this.wrapElementInPlace(info); @@ -658,8 +675,13 @@ export class IgxOverlayService implements OnDestroy { info.hook = this.placeElementHook(info.elementRef.nativeElement); info.wrapperElement = this.getWrapperElement(); const contentElement = this.getContentElement(info.wrapperElement, info.settings.modal); - const outlet = info.settings.outlet.nativeElement || info.settings.outlet; - outlet.appendChild(info.wrapperElement); + let container = info.settings.outlet?.nativeElement || info.settings.outlet; + if (info.settings.positionStrategy instanceof ContainerPositionStrategy && + info.settings.target instanceof HTMLElement + ) { + container = info.settings.target; + } + container.appendChild(info.wrapperElement); contentElement.appendChild(info.elementRef.nativeElement); } diff --git a/projects/igniteui-angular/core/src/services/overlay/position/README.md b/projects/igniteui-angular/core/src/services/overlay/position/README.md index f9339a13c02..c3eb6b20f72 100644 --- a/projects/igniteui-angular/core/src/services/overlay/position/README.md +++ b/projects/igniteui-angular/core/src/services/overlay/position/README.md @@ -1,13 +1,13 @@ # Position strategies -Position strategies determine where to display the component in the provided IgxOverlayService. There are three position strategies: +Position strategies determine where to display the component in the provided IgxOverlayService. There are five position strategies: 1) **Global** - Positions the element based on the directions passed in through PositionSettings. These are Top/Middle/Bottom for verticalDirection and Left/Center/Right for horizontalDirection. Defaults to: | horizontalDirection | verticalDirection | |:---------------------------|:-------------------------| | HorizontalAlignment.Center | VerticalAlignment.Middle | -2) **Container** - Positions the element inside the containing outlet based on the directions passed in through PositionSettings. These are Top/Middle/Bottom for verticalDirection and Left/Center/Right for horizontalDirection. **Note:** The `outlet` property in `OverlaySettings` is still supported and used by the overlay service when provided but will be removed in a future version. Avoid using this property in new code and prefer the default in-place rendering with the HTML Popover API. Defaults to: +2) **Container** - Positions the element inside a target container element. The `target` property in `OverlaySettings` must be set to an `HTMLElement` that serves as the container. The overlay wrapper is sized and positioned to match the container's bounds and automatically updates on resize via `ResizeObserver`. Directions are passed in through PositionSettings (Top/Middle/Bottom for verticalDirection and Left/Center/Right for horizontalDirection). Defaults to: | horizontalDirection | verticalDirection | |:---------------------------|:-------------------------| @@ -33,7 +33,26 @@ Position strategies determine where to display the component in the provided Igx | new Point(0, 0) | HorizontalAlignment.Right | VerticalAlignment.Bottom | HorizontalAlignment.Left | VerticalAlignment.Bottom | { width: 0, height: 0 } | ## Usage -Position an element based on an existing button as a target, so it's start point is the button's Bottom/Left corner. + +### Container positioning +Position an element inside a container using `ContainerPositionStrategy`. Set the `target` in `OverlaySettings` to the container `HTMLElement`: +```typescript +const overlaySettings: OverlaySettings = { + target: containerElement, + positionStrategy: new ContainerPositionStrategy({ + horizontalDirection: HorizontalAlignment.Center, + verticalDirection: VerticalAlignment.Middle + }), + scrollStrategy: new NoOpScrollStrategy(), + modal: false, + closeOnOutsideClick: true +}; +``` + +> **Deprecation note:** The `outlet` property in `OverlaySettings` is deprecated. Use `target` with an `HTMLElement` and `ContainerPositionStrategy` instead. + +### Connected positioning +Position an element based on an existing button as a target, so its start point is the button's Bottom/Left corner. ```typescript const positionSettings: PositionSettings = { target: buttonElement.nativeElement, @@ -44,7 +63,7 @@ const positionSettings: PositionSettings = { minSize: { width: 100, height: 300 } }; -const strategy = new ConnectedPositioningStrategy(positionSettings); +const strategy = new ConnectedPositioningStrategy(positionSettings); strategy.position(contentWrapper, size); ``` @@ -55,7 +74,7 @@ strategy.position(contentWrapper, size); Import the desired position strategy if needed like: ```typescript -import {AutoPositionStrategy, GlobalPositionStrategy, ConnectedPositioningStrategy } from './position/global-position-strategy'; +import { AutoPositionStrategy, GlobalPositionStrategy, ContainerPositionStrategy, ConnectedPositioningStrategy } from 'igniteui-angular'; ``` ## API @@ -64,7 +83,7 @@ import {AutoPositionStrategy, GlobalPositionStrategy, ConnectedPositioningStrate | Position Strategy | Name | Description | |:------------------|:-------------------------------------------------------|:----------------------------------------------------------------------------------| | Global | `position(contentElement)` | Positions the element, based on the horizontal and vertical directions. | -| Container | `position(contentElement)` | Positions the element inside the containing outlet based on the directions passed in through PositionSettings. | +| Container | `position(contentElement, size, document, initialCall, target)` | Positions the element inside the target container element. When `target` is an `HTMLElement`, the wrapper is matched to its bounds. | | Connected | `position(contentElement, size{})` | Positions the element, based on the position strategy used and the size passed in.| | Auto | `position(contentElement, size{}, document?)` | Positions the element, based on the position strategy used and the size passed in.| | Elastic | `position(contentElement, size{}, document?, minSize?)`| Positions the element, based on the position strategy used and the size passed in.| diff --git a/projects/igniteui-angular/core/src/services/overlay/position/container-position-strategy.ts b/projects/igniteui-angular/core/src/services/overlay/position/container-position-strategy.ts index da0c6f566a8..01895a1b146 100644 --- a/projects/igniteui-angular/core/src/services/overlay/position/container-position-strategy.ts +++ b/projects/igniteui-angular/core/src/services/overlay/position/container-position-strategy.ts @@ -1,4 +1,4 @@ -import { PositionSettings, Util } from '../utilities'; +import { Point, PositionSettings, Size, Util } from '../utilities'; import { GlobalPositionStrategy } from './global-position-strategy'; /** @@ -14,16 +14,20 @@ export class ContainerPositionStrategy extends GlobalPositionStrategy { /** * Position the element based on the PositionStrategy implementing this interface. */ - public override position(contentElement: HTMLElement): void { + public override position(contentElement: HTMLElement, _size?: Size, _document?: Document, _initialCall?: boolean, target?: Point | HTMLElement): void { // Set up intersection observer this.io?.disconnect(); const outletElement = contentElement.parentElement.parentElement; + if (!target || target instanceof Point || !outletElement) { + super.position(contentElement, _size, _document, _initialCall, target); + return; + } this.io = Util.setupIntersectionObserver( - outletElement, + target as HTMLElement || outletElement, contentElement.ownerDocument, - () => this.updatePosition(contentElement) + () => this.updatePosition(contentElement, target as HTMLElement) ); - this.internalPosition(contentElement); + this.internalPosition(contentElement, target as HTMLElement); } /** @@ -34,23 +38,33 @@ export class ContainerPositionStrategy extends GlobalPositionStrategy { this.io = null; } - private internalPosition(contentElement: HTMLElement): void { + private internalPosition(contentElement: HTMLElement, targetElement: HTMLElement): void { contentElement.classList.add('igx-overlay__content--relative'); contentElement.parentElement.classList.add('igx-overlay__wrapper--flex-container'); this.setPosition(contentElement); - this.updatePosition(contentElement); + this.updatePosition(contentElement, targetElement); } - private updatePosition(contentElement: HTMLElement): void { + private updatePosition(contentElement: HTMLElement, targetElement: HTMLElement): void { const outletElement = contentElement.parentElement?.parentElement; - if (!outletElement) + if (!targetElement && !outletElement) return; - // TODO: consider using new anchor() CSS function when it becomes more widely supported: https://caniuse.com/mdn-css_properties_anchor - const outletRect = outletElement.getBoundingClientRect(); - contentElement.parentElement.style.width = `${outletRect.width}px`; - contentElement.parentElement.style.height = `${outletRect.height}px`; - contentElement.parentElement.style.top = `${outletRect.top}px`; - contentElement.parentElement.style.left = `${outletRect.left}px`; + // TODO: consider using new anchor() CSS function when it becomes more widely + // supported: https://caniuse.com/mdn-css_properties_anchor + if (outletElement) { + const outletRect = outletElement.getBoundingClientRect(); + contentElement.parentElement.style.width = `${outletRect.width}px`; + contentElement.parentElement.style.height = `${outletRect.height}px`; + contentElement.parentElement.style.top = `${outletRect.top}px`; + contentElement.parentElement.style.left = `${outletRect.left}px`; + } + if (targetElement) { + const targetRect = targetElement.getBoundingClientRect(); + contentElement.parentElement.style.width = `${targetRect.width}px`; + contentElement.parentElement.style.height = `${targetRect.height}px`; + contentElement.parentElement.style.top = `${targetRect.top}px`; + contentElement.parentElement.style.left = `${targetRect.left}px`; + } } } diff --git a/projects/igniteui-angular/core/src/services/overlay/position/global-position-strategy.ts b/projects/igniteui-angular/core/src/services/overlay/position/global-position-strategy.ts index 21b55bef665..69c91047478 100644 --- a/projects/igniteui-angular/core/src/services/overlay/position/global-position-strategy.ts +++ b/projects/igniteui-angular/core/src/services/overlay/position/global-position-strategy.ts @@ -1,5 +1,5 @@ import { fadeIn, fadeOut } from 'igniteui-angular/animations'; -import { HorizontalAlignment, PositionSettings, Util, VerticalAlignment } from './../utilities'; +import { HorizontalAlignment, Point, PositionSettings, Size, Util, VerticalAlignment } from './../utilities'; import { IPositionStrategy } from './IPositionStrategy'; /** @@ -38,7 +38,7 @@ export class GlobalPositionStrategy implements IPositionStrategy { * settings.positionStrategy.position(content, size, document, true); * ``` */ - public position(contentElement: HTMLElement): void { + public position(contentElement: HTMLElement, _size?: Size, _document?: Document, _initialCall?: boolean, _target?: Point | HTMLElement): void { contentElement.classList.add('igx-overlay__content--relative'); contentElement.parentElement.classList.add('igx-overlay__wrapper--flex'); this.setPosition(contentElement); diff --git a/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts b/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts index 54a2739c8c3..30c33f3016a 100644 --- a/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts @@ -31,8 +31,7 @@ export abstract class IgxNotificationsDirective extends IgxToggleDirective /** * @deprecated The `outlet` property is deprecated and will be removed in a future version. - * While still honored by the overlay service, it should no longer be used in new code. - * Future versions will always render overlay content in place using the HTML Popover API. + * Use the `container` property instead. * * Gets/Sets the container used for the element. * @@ -42,6 +41,16 @@ export abstract class IgxNotificationsDirective extends IgxToggleDirective @Input() public outlet: IgxOverlayOutletDirective | ElementRef; + /** + * Gets/Sets the container used for the element. + * + * @remarks + * `container` is an instance of `HTMLElement`. + */ + @Input() + public container: HTMLElement; + + /** * Enables/Disables the visibility of the element. * If not set, the `isVisible` attribute will have value `false`. @@ -90,6 +99,7 @@ export abstract class IgxNotificationsDirective extends IgxToggleDirective closeOnEscape: false, closeOnOutsideClick: false, modal: false, + target: this.container, outlet: this.outlet }; diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index 56989f79b2e..cf5dc102317 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -1151,8 +1151,8 @@ export abstract class IgxGridBaseDirective implements GridType, /** * @hidden @internal */ - @ViewChild('igxLoadingOverlayOutlet', { read: IgxOverlayOutletDirective, static: true }) - public loadingOutlet: IgxOverlayOutletDirective; + @ViewChild('igxLoadingOverlayOutlet', { static: true }) + public loadingOutlet: ElementRef; /* reactContentChildren */ /* blazorInclude */ @@ -6748,7 +6748,7 @@ export abstract class IgxGridBaseDirective implements GridType, if (this.shouldOverlayLoading) { // a new overlay should be shown const overlaySettings: OverlaySettings = { - outlet: this.loadingOutlet, + target: this.loadingOutlet.nativeElement, closeOnOutsideClick: false, positionStrategy: new ContainerPositionStrategy() }; diff --git a/projects/igniteui-angular/grids/grid/src/grid.component.html b/projects/igniteui-angular/grids/grid/src/grid.component.html index 9efef0a7b42..09cb3e7e7e1 100644 --- a/projects/igniteui-angular/grids/grid/src/grid.component.html +++ b/projects/igniteui-angular/grids/grid/src/grid.component.html @@ -30,7 +30,7 @@ > -
+
- {{resourceStrings.igx_grid_snackbar_addrow_label}} + {{resourceStrings.igx_grid_snackbar_addrow_label}}
-
-
+
diff --git a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.html b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.html index 5761a4e041a..97e019b3859 100644 --- a/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.html +++ b/projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.component.html @@ -16,7 +16,7 @@ > -
+
- {{resourceStrings.igx_grid_snackbar_addrow_label}} + {{resourceStrings.igx_grid_snackbar_addrow_label}}
- -
@@ -272,7 +270,7 @@ @if (colResizingService.showResizer) { } -
+
@if (platform.isElements) {
diff --git a/projects/igniteui-angular/grids/pivot-grid/src/pivot-grid.component.html b/projects/igniteui-angular/grids/pivot-grid/src/pivot-grid.component.html index 02307066d2b..6327e864918 100644 --- a/projects/igniteui-angular/grids/pivot-grid/src/pivot-grid.component.html +++ b/projects/igniteui-angular/grids/pivot-grid/src/pivot-grid.component.html @@ -81,10 +81,10 @@
- {{resourceStrings.igx_grid_snackbar_addrow_label}} + {{resourceStrings.igx_grid_snackbar_addrow_label}}
-
+
@@ -124,7 +124,7 @@ @if (colResizingService.showResizer) { } -
+
diff --git a/projects/igniteui-angular/grids/tree-grid/src/tree-grid.component.html b/projects/igniteui-angular/grids/tree-grid/src/tree-grid.component.html index 1c5fa054b0c..3e960188a48 100644 --- a/projects/igniteui-angular/grids/tree-grid/src/tree-grid.component.html +++ b/projects/igniteui-angular/grids/tree-grid/src/tree-grid.component.html @@ -15,7 +15,7 @@ > -
+
- {{resourceStrings.igx_grid_snackbar_addrow_label}} + {{resourceStrings.igx_grid_snackbar_addrow_label}}
- -
@@ -252,7 +250,7 @@ @if (colResizingService.showResizer) { } -
+
@if (platform.isElements) {
diff --git a/projects/igniteui-angular/snackbar/README.md b/projects/igniteui-angular/snackbar/README.md index e475d13e18d..4f22927afaf 100644 --- a/projects/igniteui-angular/snackbar/README.md +++ b/projects/igniteui-angular/snackbar/README.md @@ -54,3 +54,22 @@ By default, the IgxSnackbar will be automatically hidden after 4000 milliseconds ``` You can display custom content by adding elements inside the snackbar. + +## Snackbar with a container + +You can render the snackbar inside a specific container element by setting the `container` input to an `HTMLElement`. + +```html +
+ + + + +
+``` + +When a `container` is set, the snackbar uses a `ContainerPositionStrategy` and is positioned relative to the container bounds. + +> **Note:** The `outlet` property is deprecated. Use `container` instead. diff --git a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts index cd7b50468fe..5b351bfedd0 100644 --- a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts +++ b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts @@ -156,8 +156,9 @@ export class IgxSnackbarComponent extends IgxNotificationsDirective this.textMessage = message; } - this.strategy = this.outlet ? new ContainerPositionStrategy(this.positionSettings) - : new GlobalPositionStrategy(this.positionSettings); + this.strategy = this.outlet || this.container ? + new ContainerPositionStrategy(this.positionSettings) : + new GlobalPositionStrategy(this.positionSettings); super.open(); } diff --git a/projects/igniteui-angular/toast/README.md b/projects/igniteui-angular/toast/README.md index dae0735b6af..e078caa848e 100644 --- a/projects/igniteui-angular/toast/README.md +++ b/projects/igniteui-angular/toast/README.md @@ -20,15 +20,27 @@ The toast can be shown by using the `open()` method. You can hide the toast by using the `close()` method. ## Toast Position -You can set the `positon` property to `top`, `middle`, or `bottom`, which will position the toast near the top, middle, or bottom of the document*. - -*By default the toast renders inside a global overlay outlet. You can specify a different overlay outlet by setting the `outlet` property on the toast; +You can set the `positon` property to `top`, `middle`, or `bottom`, which will position the toast near the top, middle, or bottom of the document. ```html Top Positioned Toast ``` +## Toast with a container + +You can render the toast inside a specific container element by setting the `container` input to an `HTMLElement`. + +```html +
+ Toast inside a container +
+``` + +When a `container` is set, the toast uses a `ContainerPositionStrategy` and is positioned relative to the container bounds. + +> **Note:** The `outlet` property is deprecated. Use `container` instead. + ## Toast with different content ```html diff --git a/src/app/overlay/overlay-presets.sample.html b/src/app/overlay/overlay-presets.sample.html index a115dd3af5f..5d27614747d 100644 --- a/src/app/overlay/overlay-presets.sample.html +++ b/src/app/overlay/overlay-presets.sample.html @@ -1,4 +1,4 @@ -
+
@@ -37,8 +37,8 @@ }
-
- Outlet +
+ Container
diff --git a/src/app/overlay/overlay-presets.sample.scss b/src/app/overlay/overlay-presets.sample.scss index eae389b6866..559b83f3e91 100644 --- a/src/app/overlay/overlay-presets.sample.scss +++ b/src/app/overlay/overlay-presets.sample.scss @@ -29,7 +29,7 @@ $new-card-theme: card-theme( display: block; } -.outlet{ +.container{ background-color: #444; flex: 0 0 60%; height: 500px; diff --git a/src/app/overlay/overlay-presets.sample.ts b/src/app/overlay/overlay-presets.sample.ts index bbfb28d5bdc..da77a3f4668 100644 --- a/src/app/overlay/overlay-presets.sample.ts +++ b/src/app/overlay/overlay-presets.sample.ts @@ -25,8 +25,8 @@ export class OverlayPresetsSampleComponent implements OnInit { private igxDropDown: IgxDropDownComponent; @ViewChild('button', { static: true }) private button: ElementRef; - @ViewChild('outlet', { static: true }) - private outletElement: ElementRef; + @ViewChild('container', { static: true }) + private containerElement: ElementRef; @ViewChild(IgxDragDirective, { static: true }) private igxDrag: IgxDragDirective; @@ -79,7 +79,7 @@ export class OverlayPresetsSampleComponent implements OnInit { break; case 'Container': this.relPosition = null; - this._overlaySettings = IgxOverlayService.createAbsoluteOverlaySettings(this.absPosition, this.outletElement); + this._overlaySettings = IgxOverlayService.createAbsoluteOverlaySettings(this.absPosition, this.containerElement.nativeElement); break; default: this.relPosition = null; diff --git a/src/app/overlay/overlay.sample.html b/src/app/overlay/overlay.sample.html index 01a3db1fcab..162b800894f 100644 --- a/src/app/overlay/overlay.sample.html +++ b/src/app/overlay/overlay.sample.html @@ -79,7 +79,7 @@ (change)="onSwitchChange($event)">Modal
- Open in outlet
diff --git a/src/app/overlay/overlay.sample.ts b/src/app/overlay/overlay.sample.ts index a582d666eb9..c7aae82b47c 100644 --- a/src/app/overlay/overlay.sample.ts +++ b/src/app/overlay/overlay.sample.ts @@ -39,7 +39,7 @@ export class OverlaySampleComponent implements OnInit { @ViewChild(IgxDragDirective, { static: true }) private igxDrag: IgxDragDirective; @ViewChild('outlet', { static: true }) - private outletElement: ElementRef; + private containerElement: ElementRef; public items = []; public itemsCount = 10; @@ -58,7 +58,7 @@ export class OverlaySampleComponent implements OnInit { public scrollStrategy = 'NoOp'; public closeOnOutsideClick = true; public modal = true; - public useOutlet = false; + public useContainer = false; public hasAnimation = true; public animationLength = 300; // in ms @@ -136,7 +136,7 @@ export class OverlaySampleComponent implements OnInit { this.verticalStartPoint = 'Middle'; this.closeOnOutsideClick = true; this.modal = true; - this.useOutlet = true; + this.useContainer = true; document.getElementById('mcd').classList.add('selected'); document.getElementById('mcsp').classList.add('selected'); break; @@ -235,7 +235,9 @@ export class OverlaySampleComponent implements OnInit { stringMapping['HorizontalDirection'][this.horizontalDirection]; this._overlaySettings.positionStrategy.settings.horizontalStartPoint = stringMapping['HorizontalStartPoint'][this.horizontalStartPoint]; - this._overlaySettings.outlet = this.useOutlet ? this.outletElement : null; + if (this.useContainer) { + this._overlaySettings.target = this.containerElement.nativeElement; + } } public onSwitchChange(ev: IChangeCheckboxEventArgs) { @@ -247,7 +249,7 @@ export class OverlaySampleComponent implements OnInit { this._overlaySettings.modal = ev.checked; break; case 'outlet': - this._overlaySettings.outlet = ev.checked ? this.outletElement : null; + this._overlaySettings.target = ev.checked ? this.containerElement.nativeElement : null; break; } } @@ -354,6 +356,9 @@ export class OverlaySampleComponent implements OnInit { this.cdr.detectChanges(); this.onChange2(); this._overlaySettings.target = this.button.nativeElement; + if (this.useContainer) { + this._overlaySettings.target = this.containerElement.nativeElement; + } (this._overlaySettings.positionStrategy.settings.openAnimation.options.params as IAnimationParams).duration = `${this.animationLength}ms`; (this._overlaySettings.positionStrategy.settings.closeAnimation.options.params as IAnimationParams).duration From 4293ad4091f1590699984425de01c97c3b33cbe6 Mon Sep 17 00:00:00 2001 From: wnvko Date: Fri, 13 Mar 2026 15:04:18 +0200 Subject: [PATCH 3/3] docs(notifications): update snackbar and toast samples --- .../snackbar-showcase.sample.css | 1 + .../snackbar-showcase.sample.html | 20 +++++++++---------- .../snackbar-showcase.sample.ts | 4 +--- .../toast-showcase/toast-showcase.sample.html | 3 --- .../toast-showcase/toast-showcase.sample.ts | 2 -- 5 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/app/snackbar-showcase/snackbar-showcase.sample.css b/src/app/snackbar-showcase/snackbar-showcase.sample.css index 5fff386efdd..ea6c60c9fd1 100644 --- a/src/app/snackbar-showcase/snackbar-showcase.sample.css +++ b/src/app/snackbar-showcase/snackbar-showcase.sample.css @@ -1,4 +1,5 @@ .snackbar-sample { position: relative; height: 300px; + background-color: lightgrey; } diff --git a/src/app/snackbar-showcase/snackbar-showcase.sample.html b/src/app/snackbar-showcase/snackbar-showcase.sample.html index a74cc361ea3..d08f7fcc421 100644 --- a/src/app/snackbar-showcase/snackbar-showcase.sample.html +++ b/src/app/snackbar-showcase/snackbar-showcase.sample.html @@ -1,18 +1,18 @@

Angular Snackbar vs WebComponents Snackbar

-
+
+ Angular snackbar container
Angular Snackbar Message
-
@@ -20,14 +20,12 @@

Angular Snackbar vs WebComponents Snackbar

-
- - WebCom Snackbar Message - -
+ + WebCom Snackbar Message +
diff --git a/src/app/snackbar-showcase/snackbar-showcase.sample.ts b/src/app/snackbar-showcase/snackbar-showcase.sample.ts index f970c622166..632d9c68430 100644 --- a/src/app/snackbar-showcase/snackbar-showcase.sample.ts +++ b/src/app/snackbar-showcase/snackbar-showcase.sample.ts @@ -1,8 +1,7 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef } from '@angular/core'; import { IgxButtonDirective, - IgxOverlayOutletDirective, - IgxSnackbarComponent, + IgxSnackbarComponent } from 'igniteui-angular'; import { defineComponents, IgcSnackbarComponent } from 'igniteui-webcomponents'; import { @@ -21,7 +20,6 @@ defineComponents(IgcSnackbarComponent); standalone: true, imports: [ IgxSnackbarComponent, - IgxOverlayOutletDirective, IgxButtonDirective, ], }) diff --git a/src/app/toast-showcase/toast-showcase.sample.html b/src/app/toast-showcase/toast-showcase.sample.html index 3b2ac15321a..f2749be7092 100644 --- a/src/app/toast-showcase/toast-showcase.sample.html +++ b/src/app/toast-showcase/toast-showcase.sample.html @@ -13,10 +13,7 @@

Angular Toast vs WebComponents Toast

-
- Angular Toast will self-destruct in 4 seconds. diff --git a/src/app/toast-showcase/toast-showcase.sample.ts b/src/app/toast-showcase/toast-showcase.sample.ts index fd0984fbf83..a812a596968 100644 --- a/src/app/toast-showcase/toast-showcase.sample.ts +++ b/src/app/toast-showcase/toast-showcase.sample.ts @@ -1,7 +1,6 @@ import { Component, CUSTOM_ELEMENTS_SCHEMA, DestroyRef } from '@angular/core'; import { IgxButtonDirective, - IgxOverlayOutletDirective, IgxRippleDirective, IgxToastComponent, } from 'igniteui-angular'; @@ -22,7 +21,6 @@ defineComponents(IgcToastComponent); imports: [ IgxButtonDirective, IgxRippleDirective, - IgxOverlayOutletDirective, IgxToastComponent, ], })