diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e141f451f6..a77f4ce0e50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,24 @@ All notable changes for each version of this project will be documented in this ### 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. + + - `IgxOverlayService.createAbsoluteOverlaySettings` - Added a new overload accepting `useContainerStrategy?: boolean` as the second parameter. When `true`, uses `ContainerPositionStrategy`; otherwise defaults to `GlobalPositionStrategy`. 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. + - `IgxCombo`, `IgxSimpleCombo` - Introduced the `selectionChanged` event for both components. The event is not cancelable and is emitted after the selection is committed and the component state is updated. +### General + +- `IgxOverlayService` + - **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`. + ## 21.1.3 ### Security Fixes 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..b4ac2e508df 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. | | 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 | Set the outlet container to attach the overlay to | +| outlet | IgxOverlayOutletDirective or ElementRef | **Deprecated.** Set the outlet container to attach the overlay to | ###### PositionSettings @@ -135,8 +135,8 @@ 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?| -|createRelativeOverlaySettings| Creates overlay settings with auto, connected or elastic position strategy based on a preset position settings |target, strategy?, position?| +|createAbsoluteOverlaySettings| Creates overlay settings with global or container position strategy based on a preset position settings |position?, useContainerStrategy?| +|createRelativeOverlaySettings| Creates overlay settings with auto, connected or elastic position strategy based on a preset position settings |target, strategy?, position?| ##### Events 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..b842842803f 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 create a hook element when element has a parent', () => { + 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); + + // A hidden hook div should exist in the parent to mark the element's original position + const hookElements = Array.from(parent.querySelectorAll('div[style*="display: none"]')); + expect(hookElements.length).toEqual(1); + + 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,69 +1385,74 @@ 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']); + 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); - fixture.debugElement.nativeElement.appendChild(outlet); - outlet.style.width = '800px'; - outlet.style.height = '600px'; - outlet.style.position = 'fixed'; - outlet.style.top = '100px'; - outlet.style.left = '200px'; - outlet.style.overflow = 'hidden'; + + const containerElement = fixture.componentInstance.divElement.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'; + + const overlayContent = document.createElement('div'); + overlayContent.textContent = 'Test Content'; + containerElement.appendChild(overlayContent); + fixture.detectChanges(); const positionStrategy = new ContainerPositionStrategy(); const overlaySettings: OverlaySettings = { - outlet, positionStrategy }; - const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, overlaySettings); + const id = fixture.componentInstance.overlay.attach(new ElementRef(overlayContent), overlaySettings); fixture.componentInstance.overlay.show(id); tick(); // Capture the content element while it is still in the DOM - const contentElement = fixture.nativeElement.parentElement.getElementsByClassName(CLASS_OVERLAY_CONTENT_MODAL)[0] as HTMLElement; + const contentElement = containerElement.querySelector('.' + CLASS_OVERLAY_CONTENT_MODAL) as HTMLElement; // Detaching the overlay calls dispose() on the position strategy which disconnects // the IntersectionObserver and sets it to null. However, if a callback was already @@ -1580,6 +1770,28 @@ describe('igxOverlay', () => { expect(scrollStrat.detach).toHaveBeenCalledTimes(1); })); + + 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 +1822,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 +1924,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 +2565,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 +3029,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'; @@ -3204,129 +3395,135 @@ 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 containerElement = fixture.componentInstance.divElement.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'; + + const overlayContent = document.createElement('div'); + overlayContent.textContent = 'Test Content'; + containerElement.appendChild(overlayContent); fixture.detectChanges(); const overlaySettings: OverlaySettings = { - outlet, positionStrategy: new ContainerPositionStrategy() }; - const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, overlaySettings); + const id = fixture.componentInstance.overlay.attach(new ElementRef(overlayContent), overlaySettings); fixture.componentInstance.overlay.show(id); tick(); - const overlayElement = outletElement.children[0]; + const wrapperElement = containerElement.querySelector('[popover]') as HTMLElement; + const overlayElement = wrapperElement.parentElement; 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); fixture.componentInstance.overlay.detachAll(); })); - it('Should reposition overlay when outlet is resized with ContainerPositionStrategy.', async() => { + it('Should reposition overlay when container 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 containerElement = fixture.componentInstance.divElement.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'; + + const overlayContent = document.createElement('div'); + overlayContent.textContent = 'Test Content'; + containerElement.appendChild(overlayContent); fixture.detectChanges(); const overlaySettings: OverlaySettings = { - outlet, positionStrategy: new ContainerPositionStrategy() }; - const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, overlaySettings); + const id = fixture.componentInstance.overlay.attach(new ElementRef(overlayContent), overlaySettings); fixture.componentInstance.overlay.show(id); fixture.detectChanges(); await wait(100); - let wrapperElement = outletElement.children[0]; - let wrapperRect = wrapperElement.getBoundingClientRect(); + let overlayContainer = containerElement.querySelector('[popover]').parentElement; + let containerRect = overlayContainer.getBoundingClientRect(); - // Initial wrapper dimensions should match outlet - expect(wrapperRect.width).toBeCloseTo(800, 0); - expect(wrapperRect.height).toBeCloseTo(600, 0); + // Initial container dimensions should match the positioned ancestor + expect(containerRect.width).toBeCloseTo(800, 0); + expect(containerRect.height).toBeCloseTo(600, 0); - // Resize the outlet - outletElement.style.width = '1000px'; - outletElement.style.height = '700px'; + // Resize the positioned ancestor + containerElement.style.width = '1000px'; + containerElement.style.height = '700px'; fixture.detectChanges(); await wait(100); - // Wrapper should now match new outlet dimensions - wrapperElement = outletElement.children[0]; - wrapperRect = wrapperElement.getBoundingClientRect(); - expect(wrapperRect.width).toBeCloseTo(1000, 0); - expect(wrapperRect.height).toBeCloseTo(700, 0); + // Container should now match new dimensions + overlayContainer = containerElement.querySelector('[popover]').parentElement; + containerRect = overlayContainer.getBoundingClientRect(); + expect(containerRect.width).toBeCloseTo(1000, 0); + expect(containerRect.height).toBeCloseTo(700, 0); fixture.componentInstance.overlay.detachAll(); }); - it('Should maintain centering when outlet is resized with ContainerPositionStrategy.', async () => { + it('Should maintain centering when container 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 containerElement = fixture.componentInstance.divElement.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'; + + const overlayContent = document.createElement('div'); + overlayContent.textContent = 'Test Content'; + containerElement.appendChild(overlayContent); fixture.detectChanges(); const overlaySettings: OverlaySettings = { - outlet, positionStrategy: new ContainerPositionStrategy() }; - const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, overlaySettings); + const id = fixture.componentInstance.overlay.attach(new ElementRef(overlayContent), overlaySettings); fixture.componentInstance.overlay.show(id); fixture.detectChanges(); await wait(100); - const wrapperElement = outletElement.children[0]; + const wrapperElement = containerElement.querySelector('[popover]') as HTMLElement; let componentElement = wrapperElement.children[0].children[0]; let componentRect = componentElement.getBoundingClientRect(); - let outletRect = outletElement.getBoundingClientRect(); + let containerRect = containerElement.getBoundingClientRect(); // Verify initial centering - let horizontalCenter = Math.abs((componentRect.left + componentRect.width / 2) - (outletRect.left + outletRect.width / 2)); - let verticalCenter = Math.abs((componentRect.top + componentRect.height / 2) - (outletRect.top + outletRect.height / 2)); + let horizontalCenter = Math.abs((componentRect.left + componentRect.width / 2) - (containerRect.left + containerRect.width / 2)); + let verticalCenter = Math.abs((componentRect.top + componentRect.height / 2) - (containerRect.top + containerRect.height / 2)); expect(horizontalCenter).toBeLessThan(2); expect(verticalCenter).toBeLessThan(2); - // Resize the outlet - outletElement.style.width = '900px'; - outletElement.style.height = '600px'; + // Resize the positioned ancestor + containerElement.style.width = '900px'; + containerElement.style.height = '600px'; // Wait for ResizeObserver to fire for size change await wait(100); @@ -3334,12 +3531,12 @@ describe('igxOverlay', () => { // Re-check centering with new dimensions componentElement = wrapperElement.children[0].children[0]; componentRect = componentElement.getBoundingClientRect(); - outletRect = outletElement.getBoundingClientRect(); + containerRect = 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); + horizontalCenter = Math.abs((componentRect.left + componentRect.width / 2) - (containerRect.left + containerRect.width / 2)); + verticalCenter = Math.abs((componentRect.top + componentRect.height / 2) - (containerRect.top + containerRect.height / 2)); + expect(horizontalCenter).toBeLessThan(1); + expect(verticalCenter).toBeLessThan(1); fixture.componentInstance.overlay.detachAll(); }); @@ -3347,32 +3544,34 @@ 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 containerElement = fixture.componentInstance.divElement.nativeElement; + containerElement.style.width = '800px'; + containerElement.style.height = '600px'; + containerElement.style.position = 'fixed'; + containerElement.style.top = '100px'; + containerElement.style.left = '200px'; + + const overlayContent = document.createElement('div'); + overlayContent.textContent = 'Test Content'; + containerElement.appendChild(overlayContent); fixture.detectChanges(); const positionStrategy = new ContainerPositionStrategy(); spyOn(positionStrategy, 'dispose').and.callThrough(); const overlaySettings: OverlaySettings = { - outlet, positionStrategy }; - const id = fixture.componentInstance.overlay.attach(SimpleDynamicComponent, overlaySettings); + const id = fixture.componentInstance.overlay.attach(new ElementRef(overlayContent), overlaySettings); fixture.componentInstance.overlay.show(id); await wait(100); - const wrapperElement = outletElement.children[0]; - const wrapperRect = wrapperElement.getBoundingClientRect(); + const overlayContainer = containerElement.querySelector('[popover]').parentElement; + const containerRect = overlayContainer.getBoundingClientRect(); // Initial dimensions - expect(wrapperRect.width).toBeCloseTo(800, 0); - expect(wrapperRect.height).toBeCloseTo(600, 0); + expect(containerRect.width).toBeCloseTo(800, 0); + expect(containerRect.height).toBeCloseTo(600, 0); // Detach the overlay - this should dispose the strategy fixture.componentInstance.overlay.detach(id); @@ -3522,26 +3721,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 +3750,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 +4645,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]; @@ -4636,6 +4840,7 @@ describe('igxOverlay', () => { standalone: true }) export class SimpleDynamicComponent { + public viewContainerRef = inject(ViewContainerRef); @HostBinding('style.display') public hostDisplay = 'block'; @HostBinding('style.width') @@ -4734,7 +4939,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 +4974,7 @@ export class EmptyPageInShadowDomComponent { }) export class DownRightButtonComponent { public overlay = inject(IgxOverlayService); + public viewContainerRef = inject(ViewContainerRef); @ViewChild('button', { static: true }) public buttonElement: ElementRef; @@ -4785,7 +4991,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 +5015,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 +5036,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 +5144,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..37b27051e0f 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(); @@ -138,16 +137,33 @@ export class IgxOverlayService implements OnDestroy { } /** - * Creates overlay settings with global or container position strategy and preset position settings - * * @param position Preset position settings. Default position is 'center' * @param outlet The outlet container to attach the overlay to * @returns Non-modal overlay settings based on Global or Container position strategy and the provided position. + * + * @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 */ 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 useContainerStrategy When `true`, uses `ContainerPositionStrategy` which positions the overlay + * relative to its nearest positioned ancestor container. When `false` or omitted, uses `GlobalPositionStrategy` + * which positions the overlay relative to the viewport. + * @returns Non-modal overlay settings based on `GlobalPositionStrategy` or `ContainerPositionStrategy` + * depending on the value of `useContainerStrategy`. + */ + public static createAbsoluteOverlaySettings( + position?: AbsolutePosition, useContainerStrategy?: boolean): OverlaySettings; + public static createAbsoluteOverlaySettings( + position?: AbsolutePosition, containerOrOutlet?: IgxOverlayOutletDirective | ElementRef | boolean): 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 outlet = containerOrOutlet instanceof ElementRef || containerOrOutlet instanceof IgxOverlayOutletDirective ? containerOrOutlet : null; const overlaySettings: OverlaySettings = { positionStrategy: strategy, scrollStrategy: new NoOpScrollStrategy(), @@ -294,7 +310,8 @@ export class IgxOverlayService implements OnDestroy { /** * Generates Id. Provide this Id when call `show(id)` method * - * Note created instance is in root scope, prefer the `viewContainerRef` overload when local injection context is needed. + * @note The component is created in the root scope and the overlay is attached to the document body. + * Prefer the `viewContainerRef` overload when a local injection context is needed. * * @param component Component Type to show in overlay * @param settings (optional): Create settings for the overlay, such as positioning and scroll/close behavior. @@ -330,13 +347,17 @@ 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. + info.wrapperElement = this.getWrapperElement(); + const contentElement = this.getContentElement(info.wrapperElement, info.settings.modal); + + this.insertWrapper(info); + contentElement.appendChild(info.elementRef.nativeElement); + + // Update the container size after wrapping/moving if there is size. if (info.size) { info.elementRef.nativeElement.parentElement.style.setProperty('--ig-size', info.size); } @@ -649,11 +670,59 @@ export class IgxOverlayService implements OnDestroy { return hook; } - private moveElementToOverlay(info: OverlayInfo) { - info.wrapperElement = this.getWrapperElement(); - const contentElement = this.getContentElement(info.wrapperElement, info.settings.modal); - this.getOverlayElement(info).appendChild(info.wrapperElement); - contentElement.appendChild(info.elementRef.nativeElement); + /** + * Determines the correct placement strategy for the overlay wrapper and inserts it into the DOM. + * Places a hook to mark the element's original DOM position whenever a parent exists. + * The absence of a hook indicates the element had no parent and was appended to the body. + */ + private insertWrapper(info: OverlayInfo) { + const element = info.elementRef.nativeElement; + if (element.parentElement) { + info.hook = this.placeElementHook(element); + } + + // TODO: This check for ContainerPositionStrategy is temporary and should be removed once a proper long-term + // solution for container-based positioning is in place. + if (info.settings.positionStrategy instanceof ContainerPositionStrategy) { + this.insertWrapperInContainer(info); + } else { + this.appendWrapperTo(info, this.getWrapperParent(info)); + } + } + + /** Resolves the DOM node that should host the wrapper element. */ + private getWrapperParent(info: OverlayInfo): HTMLElement { + if (info.settings.outlet) { + return info.settings.outlet.nativeElement || info.settings.outlet; + } + return info.hook?.parentElement || this._document.body; + } + + /** + * Appends the wrapper to the given parent. When a hook exists and lives under the same + * parent, the wrapper is inserted right before the original element to preserve DOM order. + */ + private appendWrapperTo(info: OverlayInfo, parent: HTMLElement) { + const ref = info.hook?.parentElement === parent ? info.elementRef.nativeElement : null; + parent.insertBefore(info.wrapperElement, ref); + } + + /** + * Creates an absolutely-positioned container div around the wrapper for ContainerPositionStrategy. + */ + private insertWrapperInContainer(info: OverlayInfo) { + const parent = info.settings.outlet?.nativeElement || + info.settings.outlet || + info.hook?.parentElement || + this._document.body; + + // TODO: container styles could be moved to CSS class + const container = this._document.createElement('div'); + container.style.position = 'absolute'; + container.style.inset = '0'; + container.style.pointerEvents = 'none'; + container.appendChild(info.wrapperElement); + parent.appendChild(container); } private getWrapperElement(): HTMLElement { @@ -684,18 +753,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 @@ -733,37 +790,83 @@ export class IgxOverlayService implements OnDestroy { } private cleanUp(info: OverlayInfo) { + this.removeWrapper(info); + this.restoreHook(info); + this.destroyComponent(info); + this.unregisterOverlay(info); + this.releaseResources(info); + } + + /** + * Reverses the wrapper insertion performed by `insertWrapper`, restoring the element to its original DOM position. + */ + private removeWrapper(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.componentRef) { - this._appRef.detachView(info.componentRef.hostView); - info.componentRef.destroy(); - delete info.componentRef; + if (!info.hook) { + // No hook means element had no parent and was appended to body. + // Just remove the wrapper; the dynamic component will be destroyed in cleanUp. + info.wrapperElement?.parentElement?.removeChild(info.wrapperElement); + } else if (info.settings.positionStrategy instanceof ContainerPositionStrategy) { + // Unwrap from container: move element back, then remove both wrapper and container div + const container = info.wrapperElement?.parentElement; + if (container) { + container.insertBefore(child, info.wrapperElement); + container.removeChild(info.wrapperElement); + container.remove(); + } + } else if (info.settings.outlet) { + 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 from outlet + if (outlet.contains(child)) { + outlet.removeChild(child.parentNode.parentNode); + } + } else { + // Unwrap in-place: move element back to wrapper's position, then remove wrapper + if (info.wrapperElement?.parentElement) { + info.wrapperElement.parentElement.insertBefore(child, info.wrapperElement); + info.wrapperElement.parentElement.removeChild(info.wrapperElement); + } } + } + + /** + * Restores the element to its original position via the hook, then removes the hook. Must run + * before `destroyComponent`. + */ + private restoreHook(info: OverlayInfo) { if (info.hook) { info.hook.parentElement.insertBefore(info.elementRef.nativeElement, info.hook); info.hook.parentElement.removeChild(info.hook); delete info.hook; } + } + + /** + * Destroys the dynamically created component, if any. + */ + private destroyComponent(info: OverlayInfo) { + if (info.componentRef) { + this._appRef.detachView(info.componentRef.hostView); + info.componentRef.destroy(); + delete info.componentRef; + } + } + /** + * Removes the overlay from the tracked list and cleans up global listeners when none remain. + */ + private unregisterOverlay(info: OverlayInfo) { 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(); } + } - // clean all the resources attached to info + /** Releases all resources attached to the overlay info. */ + private releaseResources(info: OverlayInfo) { delete info.elementRef; delete info.settings; delete info.initialSize; 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..cb8bb942bad 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: +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 trough PositionSettings. These are Top/Middle/Bottom for verticalDirection and Left/Center/Right for horizontalDirection. Defaults to: +2) **Container** - Positions the element relative to its nearest positioned ancestor. Directions are passed in through PositionSettings (Top/Middle/Bottom for verticalDirection and Left/Center/Right for horizontalDirection). 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 | |:----------------|:--------------------------|:-------------------------|:-------------------------|:-------------------------| @@ -33,7 +33,23 @@ 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 relative to its nearest positioned ancestor using `ContainerPositionStrategy`: +```typescript +const overlaySettings: OverlaySettings = { + positionStrategy: new ContainerPositionStrategy({ + horizontalDirection: HorizontalAlignment.Center, + verticalDirection: VerticalAlignment.Middle + }), + scrollStrategy: new NoOpScrollStrategy(), + modal: false, + closeOnOutsideClick: true +}; +``` + +### 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 +60,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 +71,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 +80,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 relative to its nearest positioned ancestor 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/position/container-position-strategy.ts b/projects/igniteui-angular/core/src/services/overlay/position/container-position-strategy.ts index da0c6f566a8..40a33f016f6 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 @@ -17,13 +17,17 @@ export class ContainerPositionStrategy extends GlobalPositionStrategy { public override position(contentElement: HTMLElement): void { // Set up intersection observer this.io?.disconnect(); - const outletElement = contentElement.parentElement.parentElement; + const containerElement = contentElement.parentElement.parentElement; + if (!containerElement) { + super.position(contentElement); + return; + } this.io = Util.setupIntersectionObserver( - outletElement, + containerElement, contentElement.ownerDocument, - () => this.updatePosition(contentElement) + () => this.updatePosition(contentElement, containerElement) ); - this.internalPosition(contentElement); + this.internalPosition(contentElement, containerElement); } /** @@ -34,23 +38,23 @@ export class ContainerPositionStrategy extends GlobalPositionStrategy { this.io = null; } - private internalPosition(contentElement: HTMLElement): void { + private internalPosition(contentElement: HTMLElement, container: 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, container); } - private updatePosition(contentElement: HTMLElement): void { - const outletElement = contentElement.parentElement?.parentElement; - if (!outletElement) + private updatePosition(contentElement: HTMLElement, container: HTMLElement): void { + if (!container) 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 + const containerRect = container.getBoundingClientRect(); + contentElement.parentElement.style.width = `${containerRect.width}px`; + contentElement.parentElement.style.height = `${containerRect.height}px`; + contentElement.parentElement.style.top = `${containerRect.top}px`; + contentElement.parentElement.style.left = `${containerRect.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..87514aec482 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 @@ -30,12 +30,8 @@ export class GlobalPositionStrategy implements IPositionStrategy { * Position the element based on the PositionStrategy implementing this interface. * * @param contentElement The HTML element to be positioned - * @param size Size of the element - * @param document reference to the Document object - * @param initialCall should be true if this is the initial call to the method - * @param target attaching target for the component to show * ```typescript - * settings.positionStrategy.position(content, size, document, true); + * settings.positionStrategy.position(content); * ``` */ public position(contentElement: HTMLElement): void { diff --git a/projects/igniteui-angular/core/src/services/overlay/utilities.ts b/projects/igniteui-angular/core/src/services/overlay/utilities.ts index 0cf7f38cdf0..99000179f65 100644 --- a/projects/igniteui-angular/core/src/services/overlay/utilities.ts +++ b/projects/igniteui-angular/core/src/services/overlay/utilities.ts @@ -131,7 +131,13 @@ 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 */ + /** + * Set the outlet container to attach the overlay to + * + * @deprecated in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead. + */ outlet?: IgxOverlayOutletDirective | ElementRef; /** * @hidden @internal @@ -200,7 +206,7 @@ export interface OverlayInfo { transformY?: number; event?: Event; wrapperElement?: HTMLElement; - size?: string + size?: string; } /** @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..3034888612d 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.** The container used for the pop up element. | 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..b2397454c80 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,10 @@ export class IgxCalendarContainerComponent { @HostListener('keydown.alt.arrowup', ['$event']) public onEscape(event) { event.preventDefault(); + + // Prevent the event from reaching IgxDatePickerComponent/IgxDateRangePickerComponent, + // which also handle Alt+ArrowUp and would call close() a second time. + 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..61e0a3270ab 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 @@ -245,7 +245,6 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr /** * Gets/Sets the container used for the popup element. - * * @remarks * `outlet` is an instance of `IgxOverlayOutletDirective` or an `ElementRef`. * @example @@ -255,6 +254,10 @@ export class IgxDatePickerComponent extends PickerBaseDirective implements Contr * * //.. * ``` + * + * @deprecated in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead. */ @Input() public override outlet: IgxOverlayOutletDirective | ElementRef; 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..7b56131951a 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 @@ -195,6 +195,10 @@ export abstract class PickerBaseDirective implements IToggleView, EditorProvider * * * ``` + * + * @deprecated in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead. */ @Input() public outlet: IgxOverlayOutletDirective | ElementRef; 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..8bf2293762b 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** Gets/Sets the container used for the popup element. | 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.html b/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.html index 80b9243cd6b..c36bec0f213 100644 --- a/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.html +++ b/projects/igniteui-angular/date-picker/src/date-range-picker/date-range-picker.component.html @@ -58,3 +58,5 @@ + + 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..c3c941c52d4 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 @@ -112,7 +112,6 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective private _cdr = inject(ChangeDetectorRef); private _overlayService = inject(IgxOverlayService); - /** * The number of displayed month views. * @@ -377,6 +376,10 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective * * //.. * ``` + * + * @deprecated in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead. */ @Input() public override outlet: IgxOverlayOutletDirective | ElementRef; @@ -435,7 +438,7 @@ export class IgxDateRangePickerComponent extends PickerBaseDirective @HostBinding('class.igx-date-range-picker') public cssClass = 'igx-date-range-picker'; - @ViewChild(IgxInputGroupComponent, { read: ViewContainerRef }) + @ViewChild("container", { read: ViewContainerRef }) private viewContainerRef: ViewContainerRef; /** @hidden @internal */ 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..3c036debae2 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.lastElementChild; 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.lastElementChild; 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..f9d30512b81 100644 --- a/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/notification/notifications.directive.ts @@ -34,10 +34,21 @@ export abstract class IgxNotificationsDirective extends IgxToggleDirective * * @remarks * `outlet` is an instance of `IgxOverlayOutletDirective` or an `ElementRef`. + * + * @deprecated in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead or use `container` property instead. */ @Input() public outlet: IgxOverlayOutletDirective | ElementRef; + /** + * Whether to position relative to its container instead of the viewport. + */ + @Input() + public useContainer: boolean; + + /** * Enables/Disables the visibility of the element. * If not set, the `isVisible` attribute will have value `false`. diff --git a/projects/igniteui-angular/directives/src/directives/toggle/README.md b/projects/igniteui-angular/directives/src/directives/toggle/README.md index c2529f6605e..b8503af4b43 100644 --- a/projects/igniteui-angular/directives/src/directives/toggle/README.md +++ b/projects/igniteui-angular/directives/src/directives/toggle/README.md @@ -103,7 +103,7 @@ 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.** Determines where the target overlay element should be attached. Shortcut for `overlaySettings.outlet`. # IgxOverlayOutlet Directive 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..0424bb58d75 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 in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead or use `container` property instead. */ @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..f76628bc732 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,7 @@ 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`. 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..b510ccfa156 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,13 @@ export interface AutocompleteOverlaySettings { positionStrategy?: IPositionStrategy; /** Scroll strategy to use with this settings */ scrollStrategy?: IScrollStrategy; - /** Set the outlet container to attach the overlay to */ + /** + * Set the outlet container to attach the overlay to + * + * @deprecated in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead or use `container` property instead. + */ 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..1635bb431cf 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 in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead or use `container` property instead. + */ outlet: any; /** Indicates whether the grid has columns that can be moved */ /** @hidden @internal */ 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..225446347ae 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -1150,8 +1150,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 */ @@ -4491,6 +4491,10 @@ export abstract class IgxGridBaseDirective implements GridType, * * @remarks * If set, returns the outlet defined outside the grid. Otherwise returns the grid's internal outlet directive. + * + * @deprecated in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead or use `container` property instead. */ @Input() public get outlet() { @@ -6205,6 +6209,7 @@ export abstract class IgxGridBaseDirective implements GridType, */ public showSnackbarFor(index: number) { this.addRowSnackbar.actionText = index === -1 ? '' : this.resourceStrings.igx_grid_snackbar_addrow_actiontext; + this.addRowSnackbar.useContainer = true; this.lastAddedRowIndex = index; this.addRowSnackbar.open(); } diff --git a/projects/igniteui-angular/grids/grid/src/grid-filtering-ui.spec.ts b/projects/igniteui-angular/grids/grid/src/grid-filtering-ui.spec.ts index 0b0da40b0b9..63e424f1176 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-filtering-ui.spec.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-filtering-ui.spec.ts @@ -2732,19 +2732,19 @@ describe('IgxGrid - Filtering Row UI actions #grid', () => { fix.detectChanges(); const thead = GridFunctions.getGridHeader(grid).nativeElement; - expect(thead.getBoundingClientRect().height).toEqual(grid.defaultRowHeight * 4 + 1); + expect(thead.getBoundingClientRect().height).toBeCloseTo(grid.defaultRowHeight * 4 + 1, 0); setElementSize(grid.nativeElement, ɵSize.Medium); fix.detectChanges(); await wait(100); // needed because the resize observer handler for --ig-size is called inside an angular zone fix.detectChanges(); - expect(thead.getBoundingClientRect().height).toEqual(grid.defaultRowHeight * 4 + 1); + expect(thead.getBoundingClientRect().height).toBeCloseTo(grid.defaultRowHeight * 4 + 1, 0); setElementSize(grid.nativeElement, ɵSize.Small); fix.detectChanges(); await wait(100); // needed because the resize observer handler for --ig-size is called inside an angular zone fix.detectChanges(); - expect(thead.getBoundingClientRect().height).toEqual(grid.defaultRowHeight * 4 + 1); + expect(thead.getBoundingClientRect().height).toBeCloseTo(grid.defaultRowHeight * 4 + 1, 0); }); diff --git a/projects/igniteui-angular/grids/grid/src/grid.component.html b/projects/igniteui-angular/grids/grid/src/grid.component.html index 9efef0a7b42..fc70bcd36e6 100644 --- a/projects/igniteui-angular/grids/grid/src/grid.component.html +++ b/projects/igniteui-angular/grids/grid/src/grid.component.html @@ -179,11 +179,10 @@
- {{resourceStrings.igx_grid_snackbar_addrow_label}} + {{resourceStrings.igx_grid_snackbar_addrow_label}}
-
-
+
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..0e6eb0c1fc5 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 @@ -87,6 +87,10 @@ export abstract class IgxHierarchicalGridBaseDirective extends IgxGridBaseDirect * * @remarks * If set, returns the outlet defined outside the grid. Otherwise returns the grid's internal outlet directive. + * + * @deprecated in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead or use `container` property instead. */ public override get outlet() { return this.rootGrid ? this.rootGrid.resolveOutlet() : this.resolveOutlet(); @@ -95,6 +99,10 @@ export abstract class IgxHierarchicalGridBaseDirective extends IgxGridBaseDirect /* blazorSuppress */ /** * Sets the outlet used to attach the grid's overlays to. + * + * @deprecated in version 21.2.0. Overlays now use the HTML Popover API and no longer move to the document + * body by default, so using outlet is also no longer needed - just define the overlay in the intended + * DOM tree position instead or use `container` property instead. */ public override set outlet(val: any) { this._userOutletDirective = val; 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..474b908931c 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 @@ -147,10 +147,8 @@
- {{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..f46698a5ed1 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,8 @@
- {{resourceStrings.igx_grid_snackbar_addrow_label}} + {{resourceStrings.igx_grid_snackbar_addrow_label}}
- -
@@ -124,7 +122,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..dc35157c946 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 @@ -138,10 +138,8 @@
- {{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..995f064f94e 100644 --- a/projects/igniteui-angular/snackbar/README.md +++ b/projects/igniteui-angular/snackbar/README.md @@ -54,3 +54,20 @@ 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 its nearest positioned ancestor by setting the `useContainer` input to `true`. + +```html +
+ + + + +
+``` + +When `useContainer` is `true`, the snackbar uses a `ContainerPositionStrategy` and is positioned relative to its nearest positioned ancestor. diff --git a/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts b/projects/igniteui-angular/snackbar/src/snackbar/snackbar.component.ts index cd7b50468fe..49d6ee2ed9a 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.useContainer ? + new ContainerPositionStrategy(this.positionSettings) : + new GlobalPositionStrategy(this.positionSettings); super.open(); } 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/projects/igniteui-angular/toast/README.md b/projects/igniteui-angular/toast/README.md index dae0735b6af..85d751879f1 100644 --- a/projects/igniteui-angular/toast/README.md +++ b/projects/igniteui-angular/toast/README.md @@ -20,9 +20,7 @@ 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 diff --git a/src/app/overlay/overlay-presets.sample.html b/src/app/overlay/overlay-presets.sample.html index a115dd3af5f..7db1e40c718 100644 --- a/src/app/overlay/overlay-presets.sample.html +++ b/src/app/overlay/overlay-presets.sample.html @@ -1,4 +1,4 @@ -
+
@@ -37,12 +37,15 @@ }
-
- Outlet +
+ Container +
+ I am a container shown in Global or Container position strategy. +
- @for (item of items; track item) { diff --git a/src/app/overlay/overlay-presets.sample.scss b/src/app/overlay/overlay-presets.sample.scss index eae389b6866..8b14df25213 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; @@ -37,6 +37,17 @@ $new-card-theme: card-theme( overflow: hidden; } +.container-target{ + background-color: #444; + color: white; + width: 300px; + height: 100px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; +} + .sample-dropdown{ display: flex; justify-content: center; diff --git a/src/app/overlay/overlay-presets.sample.ts b/src/app/overlay/overlay-presets.sample.ts index bbfb28d5bdc..d552ccae19f 100644 --- a/src/app/overlay/overlay-presets.sample.ts +++ b/src/app/overlay/overlay-presets.sample.ts @@ -11,24 +11,25 @@ import { IgxDropDownItemComponent, RelativePositionStrategy, AbsolutePosition, - RelativePosition + RelativePosition, + IgxToggleDirective } from 'igniteui-angular'; @Component({ selector: 'overlay-presets-sample', templateUrl: './overlay-presets.sample.html', styleUrls: ['overlay-presets.sample.scss'], - imports: [IgxRadioComponent, FormsModule, IgxButtonDirective, IgxRippleDirective, IgxDragDirective, IgxDropDownComponent, IgxDropDownItemComponent] + imports: [IgxRadioComponent, FormsModule, IgxButtonDirective, IgxRippleDirective, IgxDragDirective, IgxDropDownComponent, IgxDropDownItemComponent, IgxToggleDirective] }) export class OverlayPresetsSampleComponent implements OnInit { @ViewChild(IgxDropDownComponent, { static: true }) private igxDropDown: IgxDropDownComponent; @ViewChild('button', { static: true }) private button: ElementRef; - @ViewChild('outlet', { static: true }) - private outletElement: ElementRef; @ViewChild(IgxDragDirective, { static: true }) private igxDrag: IgxDragDirective; + @ViewChild('containerTarget', { static: true, read: IgxToggleDirective }) + private containerTarget: IgxToggleDirective; public items = []; public itemsCount = 10; @@ -79,7 +80,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, true); break; default: this.relPosition = null; @@ -87,8 +88,12 @@ export class OverlayPresetsSampleComponent implements OnInit { } } - public toggleDropDown() { + public toggle() { + if (this.positionStrategy === 'Global' || this.positionStrategy === 'Container') { + this.containerTarget.toggle(this._overlaySettings); + } else { this.igxDropDown.toggle(this._overlaySettings); + } } public onDragEnd(e) { diff --git a/src/app/overlay/overlay.sample.css b/src/app/overlay/overlay.sample.css index 3c487a7ac57..f67941745f7 100644 --- a/src/app/overlay/overlay.sample.css +++ b/src/app/overlay/overlay.sample.css @@ -1,7 +1,18 @@ -.container{ +.host{ position: relative; } +.container-target{ + background-color: #444; + color: white; + width: 300px; + height: 100px; + display: flex; + justify-content: center; + align-items: center; + text-align: center; +} + .sample-content { margin-bottom: 500px; } @@ -39,11 +50,3 @@ padding: 0 20px 20px 20px; background-color: #444444; } - -.outlet { - width: 600px; - height: 500px; - margin: 0; - padding: 0; - position: relative; -} diff --git a/src/app/overlay/overlay.sample.html b/src/app/overlay/overlay.sample.html index 01a3db1fcab..ea5317f9ed6 100644 --- a/src/app/overlay/overlay.sample.html +++ b/src/app/overlay/overlay.sample.html @@ -1,20 +1,20 @@ -
+

Translate view

- - - - - - - - - - - - + + + + + + + + + + + +
@@ -79,13 +79,17 @@ (change)="onSwitchChange($event)">Modal
- Open in outlet + Open in container
- Has animation
+
+ Change the container +
@@ -101,10 +105,7 @@
-
- Outlet -
- @for (item of items; track item) { @@ -116,4 +117,9 @@ }
+
+
+ I am a container shown in Global or Container position strategy. +
+
diff --git a/src/app/overlay/overlay.sample.ts b/src/app/overlay/overlay.sample.ts index 7029549ad1a..b80fe39d7f1 100644 --- a/src/app/overlay/overlay.sample.ts +++ b/src/app/overlay/overlay.sample.ts @@ -21,7 +21,8 @@ import { IgxButtonDirective, IgxRippleDirective, IgxDropDownItemComponent, - IChangeCheckboxEventArgs + IChangeCheckboxEventArgs, + IgxToggleDirective } from 'igniteui-angular'; import { IAnimationParams } from 'igniteui-angular/animations'; @@ -29,7 +30,7 @@ import { IAnimationParams } from 'igniteui-angular/animations'; selector: 'overlay-sample', styleUrls: ['overlay.sample.css'], templateUrl: './overlay.sample.html', - imports: [IgxRadioComponent, FormsModule, IgxSwitchComponent, IgxInputGroupComponent, IgxInputDirective, IgxLabelDirective, IgxButtonDirective, IgxRippleDirective, IgxDragDirective, IgxDropDownComponent, IgxDropDownItemComponent] + imports: [IgxRadioComponent, FormsModule, IgxSwitchComponent, IgxInputGroupComponent, IgxInputDirective, IgxLabelDirective, IgxButtonDirective, IgxRippleDirective, IgxDragDirective, IgxDropDownComponent, IgxDropDownItemComponent, IgxToggleDirective] }) export class OverlaySampleComponent implements OnInit { @ViewChild(IgxDropDownComponent, { static: true }) @@ -38,8 +39,10 @@ export class OverlaySampleComponent implements OnInit { private button: ElementRef; @ViewChild(IgxDragDirective, { static: true }) private igxDrag: IgxDragDirective; - @ViewChild('outlet', { static: true }) - private outletElement: ElementRef; + @ViewChild('container', { static: true }) + private container: ElementRef; + @ViewChild('containerTarget', { static: true, read: IgxToggleDirective }) + private containerTarget: IgxToggleDirective; public items = []; public itemsCount = 10; @@ -58,8 +61,9 @@ export class OverlaySampleComponent implements OnInit { public scrollStrategy = 'NoOp'; public closeOnOutsideClick = true; public modal = true; - public useOutlet = false; + public useContainer = false; public hasAnimation = true; + public changeContainer = false; public animationLength = 300; // in ms private xAddition = 0; @@ -136,7 +140,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 +239,6 @@ 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; } public onSwitchChange(ev: IChangeCheckboxEventArgs) { @@ -246,8 +249,24 @@ export class OverlaySampleComponent implements OnInit { case 'modal': this._overlaySettings.modal = ev.checked; break; - case 'outlet': - this._overlaySettings.outlet = ev.checked ? this.outletElement : null; + case 'container': + break; + case 'changeContainer': + if (ev.checked) { + this.container.nativeElement.style.position = 'fixed'; + this.container.nativeElement.style.width = '600px'; + this.container.nativeElement.style.height = '400px'; + this.container.nativeElement.style.border = '1px solid red'; + this.container.nativeElement.style.top = '50px'; + this.container.nativeElement.style.left = '50px'; + } else { + this.container.nativeElement.style.position = 'static'; + this.container.nativeElement.style.width = 'unset'; + this.container.nativeElement.style.height = 'unset'; + this.container.nativeElement.style.border = '0'; + this.container.nativeElement.style.top = 'unset'; + this.container.nativeElement.style.left = 'unset'; + } break; } } @@ -345,11 +364,13 @@ export class OverlaySampleComponent implements OnInit { e.target.classList.add('selected'); } - public toggleDropDown() { + public toggle() { if (this.igxDropDown.collapsed) { - this.items = []; - for (let item = 0; item < this.itemsCount; item++) { - this.items.push(`Item ${item}`); + if (this.positionStrategy !== 'Container' && this.positionStrategy !== 'Global') { + this.items = []; + for (let item = 0; item < this.itemsCount; item++) { + this.items.push(`Item ${item}`); + } } this.cdr.detectChanges(); this.onChange2(); @@ -363,7 +384,11 @@ export class OverlaySampleComponent implements OnInit { this._overlaySettings.positionStrategy.settings.closeAnimation = null; } } - this.igxDropDown.toggle(this._overlaySettings); + if (this.positionStrategy === 'Container' || this.positionStrategy === 'Global') { + this.containerTarget.toggle(this._overlaySettings); + } else { + this.igxDropDown.toggle(this._overlaySettings); + } } public ngOnInit(): void { @@ -397,7 +422,7 @@ export class OverlaySampleComponent implements OnInit { } } - protected moveContainer(target: HTMLElement, direction: string, distance: number): void{ + protected moveHost(target: HTMLElement, direction: string, distance: number): void{ const currentTop = parseInt(target.style.top, 10) || 0; const currentLeft = parseInt(target.style.left, 10) || 0; 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..1854015c943 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, ], })