diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c3ff4f70e0..c4de0d651d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,8 +22,11 @@ All notable changes for each version of this project will be documented in this - `focusedValuePipe` input property is provided that allows developers to additionally transform the value on focus; - `IgxTreeGrid`: - Batch editing - an injectable transaction provider accumulates pending changes, which are not directly applied to the grid's data source. Those can later be inspected, manipulated and submitted at once. Changes are collected for individual cells or rows, depending on editing mode, and accumulated per data row/record. - - You can now export the tree grid both to CSV and Excel. The hierarchy and the records' expanded states would be reflected in the exported Excel worksheet. + - You can now export the tree grid both to CSV and Excel. + - The hierarchy and the records' expanded states would be reflected in the exported Excel worksheet. - Summaries feature is now supported in the tree grid. Summary results are calculated and displayed for the root level and each child level by default. +- `IgxOverlayService`: + - `ElasticPositioningStrategy` added. This strategy positions the element as in **Connected** positioning strategy and resize the element to fit in the view port in case the element is partially getting out of view. ## 7.0.4 ### Bug fixes diff --git a/projects/igniteui-angular/src/lib/core/utils.spec.ts b/projects/igniteui-angular/src/lib/core/utils.spec.ts index 3454ba516b5..a1ab6e4a6b8 100644 --- a/projects/igniteui-angular/src/lib/core/utils.spec.ts +++ b/projects/igniteui-angular/src/lib/core/utils.spec.ts @@ -161,6 +161,14 @@ describe('Utils', () => { expect(clone.Null).toBeNull(); expect(clone.undefined).toBeUndefined(); }); + + it('Should correctly handle null and undefined values', () => { + const nullClone = cloneValue(null); + expect(nullClone).toBeNull(); + + const undefinedClone = cloneValue(undefined); + expect(undefinedClone).toBeUndefined(); + }); }); describe('Utils - mergeObjects() unit tests', () => { diff --git a/projects/igniteui-angular/src/lib/grids/grid.common.ts b/projects/igniteui-angular/src/lib/grids/grid.common.ts index f4b666386ad..419f62a948c 100644 --- a/projects/igniteui-angular/src/lib/grids/grid.common.ts +++ b/projects/igniteui-angular/src/lib/grids/grid.common.ts @@ -611,7 +611,11 @@ export class ContainerPositioningStrategy extends ConnectedPositioningStrategy { this.settings.verticalStartPoint = this.isTop ? VerticalAlignment.Top : VerticalAlignment.Bottom; this.settings.openAnimation = this.isTop ? scaleInVerBottom : scaleInVerTop; const startPoint = getPointFromPositionsSettings(this.settings, contentElement.parentElement); - contentElement.style.top = startPoint.y + (this.isTop ? VerticalAlignment.Top : VerticalAlignment.Bottom) * size.height + 'px'; + + // TODO: extract transform setting in util function + const translateY = startPoint.y + (this.isTop ? VerticalAlignment.Top : VerticalAlignment.Bottom) * size.height; + const translateYString = `translateY(${translateY}px)`; + contentElement.style.transform = contentElement.style.transform.replace(/translateY\([.-\d]+px\)/g, translateYString); contentElement.style.width = target.clientWidth + 'px'; } } diff --git a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts index 775cc8bb78a..b6e858f7f6a 100644 --- a/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts +++ b/projects/igniteui-angular/src/lib/grids/grid/grid-filtering-ui.spec.ts @@ -1293,7 +1293,7 @@ describe('IgxGrid - Filtering actions', () => { tick(); fix.detectChanges(); - const firstMonth = calendar.queryAll(By.css('.igx-calendar__month'))[0]; + const firstMonth = calendar.queryAll(By.css(`[class*='igx-calendar__month']`))[0]; firstMonth.nativeElement.click(); tick(); fix.detectChanges(); diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts index 54bdacf30b7..fb9a5c92c8d 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.spec.ts @@ -7,7 +7,7 @@ import { ComponentRef, HostBinding } from '@angular/core'; -import { async as asyncWrapper, TestBed, fakeAsync, tick, async } from '@angular/core/testing'; +import { async as asyncWrapper, TestBed, fakeAsync, tick, async, ComponentFixture } from '@angular/core/testing'; import { BrowserModule } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { IgxOverlayService } from './overlay'; @@ -15,6 +15,7 @@ import { IgxToggleDirective, IgxToggleModule, IgxOverlayOutletDirective } from ' import { AutoPositionStrategy } from './position/auto-position-strategy'; import { ConnectedPositioningStrategy } from './position/connected-positioning-strategy'; import { GlobalPositionStrategy } from './position/global-position-strategy'; +import { ElasticPositionStrategy } from './position/elastic-position-strategy'; import { PositionSettings, HorizontalAlignment, @@ -36,6 +37,7 @@ import { configureTestSuite } from '../../test-utils/configure-suite'; import { IgxCalendarComponent, IgxCalendarModule } from '../../calendar/index'; import { IgxAvatarComponent, IgxAvatarModule } from '../../avatar/avatar.component'; import { IgxDatePickerComponent, IgxDatePickerModule } from '../../date-picker/date-picker.component'; +import { IPositionStrategy } from './position/IPositionStrategy'; const CLASS_OVERLAY_CONTENT = 'igx-overlay__content'; const CLASS_OVERLAY_CONTENT_MODAL = 'igx-overlay__content--modal'; @@ -112,6 +114,52 @@ function getExpectedLeftPosition(horizontalAlignment: HorizontalAlignment, eleme return expectedLeft; } +function 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 { + location.x = targetRect.right; + } + } else if (location.x + wrapperRect.width > screenRect.right && !elastic) { + location.x = targetRect.left - wrapperRect.width; + } + + 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 { + location.y = targetRect.bottom; + } + } else if (location.y + wrapperRect.height > screenRect.bottom && !elastic) { + location.y = targetRect.top - wrapperRect.height; + } + return location; +} + describe('igxOverlay', () => { beforeEach(async(() => { UIInteractions.clearOverlay(); @@ -274,6 +322,7 @@ describe('igxOverlay', () => { spyOn(overlayInstance.onClosing, 'emit'); spyOn(overlayInstance.onOpened, 'emit'); spyOn(overlayInstance.onOpening, 'emit'); + spyOn(overlayInstance.onAnimation, 'emit'); const firstCallId = overlayInstance.show(SimpleDynamicComponent); tick(); @@ -283,6 +332,7 @@ describe('igxOverlay', () => { .toHaveBeenCalledWith({ id: firstCallId, componentRef: jasmine.any(ComponentRef), cancel: false }); const args: OverlayEventArgs = (overlayInstance.onOpening.emit as jasmine.Spy).calls.mostRecent().args[0]; expect(args.componentRef.instance).toEqual(jasmine.any(SimpleDynamicComponent)); + expect(overlayInstance.onAnimation.emit).toHaveBeenCalledTimes(1); tick(); expect(overlayInstance.onOpened.emit).toHaveBeenCalledTimes(1); @@ -293,6 +343,7 @@ describe('igxOverlay', () => { expect(overlayInstance.onClosing.emit).toHaveBeenCalledTimes(1); expect(overlayInstance.onClosing.emit) .toHaveBeenCalledWith({ id: firstCallId, componentRef: jasmine.any(ComponentRef), cancel: false }); + expect(overlayInstance.onAnimation.emit).toHaveBeenCalledTimes(2); tick(); expect(overlayInstance.onClosed.emit).toHaveBeenCalledTimes(1); @@ -302,6 +353,7 @@ describe('igxOverlay', () => { tick(); expect(overlayInstance.onOpening.emit).toHaveBeenCalledTimes(2); expect(overlayInstance.onOpening.emit).toHaveBeenCalledWith({ componentRef: undefined, id: secondCallId, cancel: false }); + expect(overlayInstance.onAnimation.emit).toHaveBeenCalledTimes(3); tick(); expect(overlayInstance.onOpened.emit).toHaveBeenCalledTimes(2); @@ -311,13 +363,14 @@ describe('igxOverlay', () => { tick(); expect(overlayInstance.onClosing.emit).toHaveBeenCalledTimes(2); expect(overlayInstance.onClosing.emit).toHaveBeenCalledWith({ componentRef: undefined, id: secondCallId, cancel: false }); + expect(overlayInstance.onAnimation.emit).toHaveBeenCalledTimes(4); tick(); expect(overlayInstance.onClosed.emit).toHaveBeenCalledTimes(2); expect(overlayInstance.onClosed.emit).toHaveBeenCalledWith({ componentRef: undefined, id: secondCallId }); })); - it('Should properly call position method - GlobalPosition.', () => { + it('Should properly set style on position method call - GlobalPosition.', () => { const mockParent = document.createElement('div'); const mockItem = document.createElement('div'); mockParent.appendChild(mockItem); @@ -345,7 +398,7 @@ describe('igxOverlay', () => { } }); - it('Should properly call position method - ConnectedPosition.', () => { + it('Should properly set style on position method call - ConnectedPosition.', () => { const mockParent = jasmine.createSpyObj('parentElement', ['style', 'lastElementChild']); const mockItem = document.createElement('div'); let width = 200; @@ -367,18 +420,15 @@ describe('igxOverlay', () => { }; const connectedStrat1 = new ConnectedPositioningStrategy(mockPositioningSettings1); connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('0px'); - expect(mockItem.style.left).toEqual('-200px'); + expect(mockItem.style.transform).toEqual('translateX(-200px) translateY(0px)'); connectedStrat1.settings.horizontalStartPoint = HorizontalAlignment.Center; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('0px'); - expect(mockItem.style.left).toEqual('-100px'); + expect(mockItem.style.transform).toEqual('translateX(-100px) translateY(0px)'); connectedStrat1.settings.horizontalStartPoint = HorizontalAlignment.Right; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('0px'); - expect(mockItem.style.left).toEqual('0px'); + expect(mockItem.style.transform).toEqual('translateX(0px) translateY(0px)'); right = 0; bottom = 0; @@ -386,18 +436,14 @@ describe('igxOverlay', () => { height = 200; connectedStrat1.settings.verticalStartPoint = VerticalAlignment.Top; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('-200px'); - expect(mockItem.style.left).toEqual('0px'); - + expect(mockItem.style.transform).toEqual('translateX(0px) translateY(-200px)'); connectedStrat1.settings.verticalStartPoint = VerticalAlignment.Middle; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('-100px'); - expect(mockItem.style.left).toEqual('0px'); + expect(mockItem.style.transform).toEqual('translateX(0px) translateY(-100px)'); connectedStrat1.settings.verticalStartPoint = VerticalAlignment.Bottom; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('0px'); - expect(mockItem.style.left).toEqual('0px'); + expect(mockItem.style.transform).toEqual('translateX(0px) translateY(0px)'); right = 0; bottom = 0; @@ -405,18 +451,15 @@ describe('igxOverlay', () => { height = 0; connectedStrat1.settings.verticalDirection = VerticalAlignment.Top; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('-200px'); - expect(mockItem.style.left).toEqual('0px'); + expect(mockItem.style.transform).toEqual('translateX(0px) translateY(-200px)'); connectedStrat1.settings.verticalDirection = VerticalAlignment.Middle; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('-100px'); - expect(mockItem.style.left).toEqual('0px'); + expect(mockItem.style.transform).toEqual('translateX(0px) translateY(-100px)'); connectedStrat1.settings.verticalDirection = VerticalAlignment.Bottom; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('0px'); - expect(mockItem.style.left).toEqual('0px'); + expect(mockItem.style.transform).toEqual('translateX(0px) translateY(0px)'); right = 0; bottom = 0; @@ -424,34 +467,29 @@ describe('igxOverlay', () => { height = 0; connectedStrat1.settings.horizontalDirection = HorizontalAlignment.Left; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('0px'); - expect(mockItem.style.left).toEqual('-200px'); + expect(mockItem.style.transform).toEqual('translateX(-200px) translateY(0px)'); connectedStrat1.settings.horizontalDirection = HorizontalAlignment.Center; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('0px'); - expect(mockItem.style.left).toEqual('-100px'); + expect(mockItem.style.transform).toEqual('translateX(-100px) translateY(0px)'); connectedStrat1.settings.horizontalDirection = HorizontalAlignment.Right; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('0px'); - expect(mockItem.style.left).toEqual('0px'); + expect(mockItem.style.transform).toEqual('translateX(0px) translateY(0px)'); // If target is Point connectedStrat1.settings.target = new Point(0, 0); connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('0px'); - expect(mockItem.style.left).toEqual('0px'); + expect(mockItem.style.transform).toEqual('translateX(0px) translateY(0px)'); // If target is not point or html element, should fallback to new Point(0,0) connectedStrat1.settings.target = 'g'; connectedStrat1.position(mockItem, { width: 200, height: 200 }); - expect(mockItem.style.top).toEqual('0px'); - expect(mockItem.style.left).toEqual('0px'); + expect(mockItem.style.transform).toEqual('translateX(0px) translateY(0px)'); }); it('Should properly call position method - AutoPosition.', () => { - const mockParent = jasmine.createSpyObj('parentElement', ['style', 'lastElementChild']); + const mockParent = jasmine.createSpyObj('parentElement', ['style', 'lastElementChild', 'getBoundingClientRect']); const mockItem = { parentElement: mockParent, clientHeight: 0, clientWidth: 0 } as HTMLElement; spyOn(mockItem, 'parentElement').and.returnValue(mockParent); const mockPositioningSettings1: PositionSettings = { @@ -462,14 +500,12 @@ describe('igxOverlay', () => { verticalStartPoint: VerticalAlignment.Top }; const autoStrat1 = new AutoPositionStrategy(mockPositioningSettings1); - spyOn(autoStrat1, 'getViewPort').and.returnValue(jasmine.createSpyObj('obj', ['left', 'top', 'right', 'bottom'])); spyOn(ConnectedPositioningStrategy.prototype, 'position'); + mockParent.getBoundingClientRect.and.returnValue(jasmine.createSpyObj('obj', ['left', 'top'])); - autoStrat1.position(mockItem.parentElement, null, null, true); - expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledTimes(2); + autoStrat1.position(mockItem.parentElement, null, null, true, null); + expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledTimes(1); expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledWith(mockItem.parentElement, null); - expect(autoStrat1.getViewPort).toHaveBeenCalledWith(null); - expect(autoStrat1.getViewPort).toHaveBeenCalledTimes(1); const mockPositioningSettings2: PositionSettings = { horizontalDirection: HorizontalAlignment.Left, @@ -479,12 +515,9 @@ describe('igxOverlay', () => { verticalStartPoint: VerticalAlignment.Top }; const autoStrat2 = new AutoPositionStrategy(mockPositioningSettings2); - spyOn(autoStrat2, 'getViewPort').and.returnValue(jasmine.createSpyObj('obj', ['left', 'top', 'right', 'bottom'])); - autoStrat2.position(mockItem.parentElement, null, null, true); - expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledTimes(4); - expect(autoStrat2.getViewPort).toHaveBeenCalledWith(null); - expect(autoStrat2.getViewPort).toHaveBeenCalledTimes(1); + autoStrat2.position(mockItem.parentElement, null, null, true, null); + expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledTimes(2); const mockPositioningSettings3: PositionSettings = { horizontalDirection: HorizontalAlignment.Center, @@ -494,34 +527,53 @@ describe('igxOverlay', () => { verticalStartPoint: VerticalAlignment.Top }; const autoStrat3 = new AutoPositionStrategy(mockPositioningSettings3); - spyOn(autoStrat3, 'getViewPort').and.returnValue(jasmine.createSpyObj('obj', ['left', 'top', 'right', 'bottom'])); autoStrat3.position(mockItem.parentElement, null, null); - expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledTimes(5); - expect(autoStrat3.getViewPort).toHaveBeenCalledTimes(0); + expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledTimes(3); }); - it('Should properly call AutoPosition getViewPort.', () => { - const autoStrat1 = new AutoPositionStrategy(); - const docSpy = { - documentElement: { - getBoundingClientRect: () => { - return { - top: 1920, - left: 768 - }; - } - } + it('Should properly call position method - ElasticPosition.', () => { + const mockParent = jasmine.createSpyObj('parentElement', ['style', 'lastElementChild', 'getBoundingClientRect']); + const mockItem = { parentElement: mockParent, clientHeight: 0, clientWidth: 0 } as HTMLElement; + spyOn(mockItem, 'parentElement').and.returnValue(mockParent); + const mockPositioningSettings1: PositionSettings = { + horizontalDirection: HorizontalAlignment.Right, + verticalDirection: VerticalAlignment.Bottom, + target: mockItem, + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Top }; - spyOn(document, 'documentElement').and.returnValue(1); - expect(autoStrat1.getViewPort(docSpy)).toEqual({ - top: -1920, - left: -768, - bottom: -1920 + window.innerHeight, - right: -768 + + window.innerWidth, - height: window.innerHeight, - width: window.innerWidth - }); + const autoStrat1 = new ElasticPositionStrategy(mockPositioningSettings1); + spyOn(ConnectedPositioningStrategy.prototype, 'position'); + mockParent.getBoundingClientRect.and.returnValue(jasmine.createSpyObj('obj', ['left', 'top'])); + + autoStrat1.position(mockItem.parentElement, null, null, true, null); + expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledTimes(1); + expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledWith(mockItem.parentElement, null); + + const mockPositioningSettings2: PositionSettings = { + horizontalDirection: HorizontalAlignment.Left, + verticalDirection: VerticalAlignment.Top, + target: mockItem, + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Top + }; + const autoStrat2 = new ElasticPositionStrategy(mockPositioningSettings2); + + autoStrat2.position(mockItem.parentElement, null, null, true, null); + expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledTimes(2); + + const mockPositioningSettings3: PositionSettings = { + horizontalDirection: HorizontalAlignment.Center, + verticalDirection: VerticalAlignment.Middle, + target: mockItem, + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Top + }; + const autoStrat3 = new ElasticPositionStrategy(mockPositioningSettings3); + + autoStrat3.position(mockItem.parentElement, null, null); + expect(ConnectedPositioningStrategy.prototype.position).toHaveBeenCalledTimes(3); }); it('fix for #1690 - click on second filter does not close first one.', fakeAsync(() => { @@ -599,25 +651,25 @@ describe('igxOverlay', () => { })); it('fix for #2475 - An error is thrown for IgxOverlay when showing a component' + - 'instance that is not attached to the DOM', fakeAsync(() => { - const fix = TestBed.createComponent(SimpleRefComponent); - fix.detectChanges(); - fix.elementRef.nativeElement.parentElement.removeChild(fix.elementRef.nativeElement); - fix.componentInstance.overlay.show(fix.elementRef); - - tick(); - const overlayDiv = document.getElementsByClassName(CLASS_OVERLAY_MAIN)[0]; - expect(overlayDiv).toBeDefined(); - expect(overlayDiv.children.length).toEqual(1); - const wrapperDiv = overlayDiv.children[0]; - expect(wrapperDiv).toBeDefined(); - expect(wrapperDiv.classList.contains(CLASS_OVERLAY_WRAPPER_MODAL)).toBeTruthy(); - expect(wrapperDiv.children[0].localName).toEqual('div'); + 'instance that is not attached to the DOM', fakeAsync(() => { + const fix = TestBed.createComponent(SimpleRefComponent); + fix.detectChanges(); + fix.elementRef.nativeElement.parentElement.removeChild(fix.elementRef.nativeElement); + fix.componentInstance.overlay.show(fix.elementRef); - const contentDiv = wrapperDiv.children[0]; - expect(contentDiv).toBeDefined(); - expect(contentDiv.classList.contains(CLASS_OVERLAY_CONTENT_MODAL)).toBeTruthy(); - })); + tick(); + const overlayDiv = document.getElementsByClassName(CLASS_OVERLAY_MAIN)[0]; + expect(overlayDiv).toBeDefined(); + expect(overlayDiv.children.length).toEqual(1); + const wrapperDiv = overlayDiv.children[0]; + expect(wrapperDiv).toBeDefined(); + expect(wrapperDiv.classList.contains(CLASS_OVERLAY_WRAPPER_MODAL)).toBeTruthy(); + expect(wrapperDiv.children[0].localName).toEqual('div'); + + const contentDiv = wrapperDiv.children[0]; + expect(contentDiv).toBeDefined(); + expect(contentDiv.classList.contains(CLASS_OVERLAY_CONTENT_MODAL)).toBeTruthy(); + })); it('fix for #2486 - filtering dropdown is not correctly positioned', fakeAsync(() => { const fix = TestBed.createComponent(WidthTestOverlayComponent); @@ -639,7 +691,7 @@ describe('igxOverlay', () => { expect(fix.componentInstance.customComponent.nativeElement.getBoundingClientRect().left).toBe(400); })); - it('fix for @2798 - Allow canceling of open and close of IgxDropDown through onOpening and onClosing events', fakeAsync(() => { + it('fix for #2798 - Allow canceling of open and close of IgxDropDown through onOpening and onClosing events', fakeAsync(() => { const fix = TestBed.createComponent(SimpleRefComponent); fix.detectChanges(); const overlayInstance = fix.componentInstance.overlay; @@ -677,7 +729,7 @@ describe('igxOverlay', () => { })); }); - describe('Unit Tests p2 (overrides): ', () => { + describe('Unit Tests - Scroll Strategies: ', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [IgxToggleModule, DynamicModule, NoopAnimationsModule], @@ -687,7 +739,7 @@ describe('igxOverlay', () => { afterAll(() => { TestBed.resetTestingModule(); }); - it('Should properly initialize Scroll Strategy - Block.', fakeAsync( async () => { + it('Should properly initialize Scroll Strategy - Block.', fakeAsync(async () => { TestBed.overrideComponent(EmptyPageComponent, { set: { styles: [`button { @@ -818,31 +870,30 @@ describe('igxOverlay', () => { // 1. Positioning Strategies // 1.1 Global (show components in the window center - default). - it('Should render igx-overlay on top of all other views/components (any previously existing html on the page) etc.', - fakeAsync(() => { - const fixture = TestBed.createComponent(EmptyPageComponent); - fixture.detectChanges(); - const overlaySettings: OverlaySettings = { - positionStrategy: new GlobalPositionStrategy(), - scrollStrategy: new NoOpScrollStrategy(), - modal: false, - closeOnOutsideClick: false - }; - const positionSettings: PositionSettings = { - horizontalDirection: HorizontalAlignment.Right, - verticalDirection: VerticalAlignment.Bottom, - target: fixture.componentInstance.buttonElement.nativeElement, - horizontalStartPoint: HorizontalAlignment.Left, - verticalStartPoint: VerticalAlignment.Top - }; - overlaySettings.positionStrategy = new GlobalPositionStrategy(positionSettings); - fixture.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); - tick(); - fixture.detectChanges(); - const overlayDiv = document.getElementsByClassName(CLASS_OVERLAY_MAIN)[0]; - const wrapper = overlayDiv.children[0]; - expect(wrapper.classList).toContain(CLASS_OVERLAY_WRAPPER); - })); + it('Should correctly render igx-overlay', fakeAsync(() => { + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); + const overlaySettings: OverlaySettings = { + positionStrategy: new GlobalPositionStrategy(), + scrollStrategy: new NoOpScrollStrategy(), + modal: false, + closeOnOutsideClick: false + }; + const positionSettings: PositionSettings = { + horizontalDirection: HorizontalAlignment.Right, + verticalDirection: VerticalAlignment.Bottom, + target: fixture.componentInstance.buttonElement.nativeElement, + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Top + }; + overlaySettings.positionStrategy = new GlobalPositionStrategy(positionSettings); + fixture.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + fixture.detectChanges(); + const overlayDiv = document.getElementsByClassName(CLASS_OVERLAY_MAIN)[0]; + const wrapper = overlayDiv.children[0]; + expect(wrapper.classList).toContain(CLASS_OVERLAY_WRAPPER); + })); it('Should cover the whole window 100% width and height.', fakeAsync(() => { const fixture = TestBed.createComponent(EmptyPageComponent); @@ -855,11 +906,10 @@ describe('igxOverlay', () => { const overlayDiv = document.getElementsByClassName(CLASS_OVERLAY_MAIN)[0]; const overlayWrapper = overlayDiv.children[0]; const overlayRect = overlayWrapper.getBoundingClientRect(); - const windowRect = document.body.getBoundingClientRect(); - expect(overlayRect.width).toEqual(windowRect.width); - expect(overlayRect.height).toEqual(windowRect.height); - expect(overlayRect.left).toEqual(windowRect.left); - expect(overlayRect.top).toEqual(windowRect.top); + expect(overlayRect.width).toEqual(window.innerWidth); + expect(overlayRect.height).toEqual(window.innerHeight); + expect(overlayRect.left).toEqual(0); + expect(overlayRect.top).toEqual(0); })); it('Should show the component inside the igx-overlay wrapper as a content last child.', fakeAsync(() => { @@ -929,12 +979,9 @@ describe('igxOverlay', () => { const overlayDiv = document.getElementsByClassName(CLASS_OVERLAY_MAIN)[0]; const overlayWrapper = overlayDiv.children[0] as HTMLElement; const componentEl = overlayWrapper.children[0].children[0]; - const wrapperRect = overlayWrapper.getBoundingClientRect(); const componentRect = componentEl.getBoundingClientRect(); - expect(wrapperRect.width / 2 - componentRect.width / 2).toEqual(componentRect.left); - expect(wrapperRect.height / 2 - componentRect.height / 2).toEqual(componentRect.top); - expect(componentRect.left).toEqual(componentRect.right - componentRect.width); - expect(componentRect.top).toEqual(componentRect.bottom - componentRect.height); + expect((window.innerWidth - componentRect.width) / 2).toEqual(componentRect.left); + expect((window.innerHeight - componentRect.height) / 2).toEqual(componentRect.top); })); it('Should display a new instance of the same component/options exactly on top of the previous one.', fakeAsync(() => { @@ -1017,7 +1064,7 @@ describe('igxOverlay', () => { })); // 1.2 ConnectedPositioningStrategy(show components based on a specified position base point, horizontal and vertical alignment) - it('Should render on top of all other views/components (any previously existing html on the page) etc.', fakeAsync(() => { + it('Should correctly render igx-overlay', fakeAsync(() => { const fixture = TestBed.createComponent(EmptyPageComponent); fixture.detectChanges(); const overlaySettings: OverlaySettings = { @@ -1061,9 +1108,8 @@ describe('igxOverlay', () => { fixture.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); tick(); const wrapper = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; - const body = document.getElementsByTagName('body')[0]; - expect(wrapper.clientHeight).toEqual(body.clientHeight); - expect(wrapper.clientWidth).toEqual(body.clientWidth); + expect(wrapper.clientHeight).toEqual(window.innerHeight); + expect(wrapper.clientWidth).toEqual(window.innerWidth); })); it('It should position the shown component inside the igx-overlay wrapper as a content last child.', fakeAsync(() => { @@ -1103,13 +1149,14 @@ describe('igxOverlay', () => { horizontalStartPoint: HorizontalAlignment.Left, verticalStartPoint: VerticalAlignment.Bottom, openAnimation: scaleInVerTop, - closeAnimation: scaleOutVerTop + closeAnimation: scaleOutVerTop, + minSize: { width: 0, height: 0 } }; expect(strategy.settings).toEqual(expectedDefaults); }); - it(`Should use target: null StartPoint:Left/Bottom, Direction Right/Bottom and openAnimation: scaleInVerTop, + it(`Should use target: null StartPoint:Left/Bottom, Direction Right/Bottom and openAnimation: scaleInVerTop, closeAnimation: scaleOutVerTop as default options when using a ConnectedPositioningStrategy without passing options.`, () => { const strategy = new ConnectedPositioningStrategy(); @@ -1120,7 +1167,8 @@ describe('igxOverlay', () => { horizontalStartPoint: HorizontalAlignment.Left, verticalStartPoint: VerticalAlignment.Bottom, openAnimation: scaleInVerTop, - closeAnimation: scaleOutVerTop + closeAnimation: scaleOutVerTop, + minSize: { width: 0, height: 0 } }; expect(strategy.settings).toEqual(expectedDefaults); @@ -1320,8 +1368,8 @@ describe('igxOverlay', () => { const strategy = new ConnectedPositioningStrategy(positionSettings2); strategy.position(contentWrapper, size); fixture.detectChanges(); - expect(contentWrapper.style.top).toBe(expectedTopForPoint[j]); - expect(contentWrapper.style.left).toBe(expectedLeftForPoint[i]); + const transform = `translateX(${expectedLeftForPoint[i]}) translateY(${expectedTopForPoint[j]})`; + expect(contentWrapper.style.transform).toBe(transform); } } document.body.removeChild(contentWrapper); @@ -1368,8 +1416,10 @@ describe('igxOverlay', () => { const strategy = new ConnectedPositioningStrategy(positionSettings2); strategy.position(contentWrapper, size); fixture.detectChanges(); - expect(contentWrapper.style.top).toBe((expectedTopForPoint[j] + 30 * tsp) + 'px'); - expect(contentWrapper.style.left).toBe((expectedLeftForPoint[i] + 50 * lsp) + 'px'); + const translateY = (expectedTopForPoint[j] + 30 * tsp) + 'px'; + const translateX = (expectedLeftForPoint[i] + 50 * lsp) + 'px'; + const transform = `translateX(${translateX}) translateY(${translateY})`; + expect(contentWrapper.style.transform).toBe(transform); } } } @@ -1378,32 +1428,31 @@ describe('igxOverlay', () => { }); // 1.3 AutoPosition (fit the shown component into the visible window.) - it('Should render igx-overlay on top of all other views/components (any previously existing html on the page) etc.', - fakeAsync(() => { - const fix = TestBed.createComponent(EmptyPageComponent); - fix.detectChanges(); - const overlaySettings: OverlaySettings = { - positionStrategy: new GlobalPositionStrategy(), - scrollStrategy: new NoOpScrollStrategy(), - modal: false, - closeOnOutsideClick: false - }; + it('Should correctly render igx-overlay', fakeAsync(() => { + const fix = TestBed.createComponent(EmptyPageComponent); + fix.detectChanges(); + const overlaySettings: OverlaySettings = { + positionStrategy: new AutoPositionStrategy(), + scrollStrategy: new NoOpScrollStrategy(), + modal: false, + closeOnOutsideClick: false + }; - const positionSettings: PositionSettings = { - horizontalDirection: HorizontalAlignment.Right, - verticalDirection: VerticalAlignment.Bottom, - target: fix.componentInstance.buttonElement.nativeElement, - horizontalStartPoint: HorizontalAlignment.Left, - verticalStartPoint: VerticalAlignment.Top - }; - overlaySettings.positionStrategy = new AutoPositionStrategy(positionSettings); - fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); - tick(); - fix.detectChanges(); - const wrapper = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; - expect(wrapper).toBeDefined(); - expect(wrapper.classList).toContain(CLASS_OVERLAY_WRAPPER); - })); + const positionSettings: PositionSettings = { + horizontalDirection: HorizontalAlignment.Right, + verticalDirection: VerticalAlignment.Bottom, + target: fix.componentInstance.buttonElement.nativeElement, + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Top + }; + overlaySettings.positionStrategy = new AutoPositionStrategy(positionSettings); + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + fix.detectChanges(); + const wrapper = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; + expect(wrapper).toBeDefined(); + expect(wrapper.classList).toContain(CLASS_OVERLAY_WRAPPER); + })); it('Should cover the whole window 100% width and height.', fakeAsync(() => { const fix = TestBed.createComponent(EmptyPageComponent); @@ -1426,9 +1475,8 @@ describe('igxOverlay', () => { tick(); fix.detectChanges(); const wrapper = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; - const body = document.getElementsByTagName('body')[0]; - expect(wrapper.clientHeight).toEqual(body.clientHeight); - expect(wrapper.clientWidth).toEqual(body.clientWidth); + expect(wrapper.clientHeight).toEqual(window.innerHeight); + expect(wrapper.clientWidth).toEqual(window.innerWidth); })); it('Should append the shown component inside the igx-overlay as a last child.', fakeAsync(() => { @@ -1462,6 +1510,8 @@ describe('igxOverlay', () => { it('Should show the component inside of the viewport if it would normally be outside of bounds, BOTTOM + RIGHT.', fakeAsync(() => { const fix = TestBed.createComponent(DownRightButtonComponent); fix.detectChanges(); + + fix.componentInstance.positionStrategy = new AutoPositionStrategy(); const currentElement = fix.componentInstance; const buttonElement = fix.componentInstance.buttonElement.nativeElement; fix.detectChanges(); @@ -1483,8 +1533,8 @@ describe('igxOverlay', () => { const buttonTop = buttonElement.offsetTop; const expectedLeft = buttonLeft - wrapperContent.lastElementChild.lastElementChild.clientWidth; const expectedTop = buttonTop - wrapperContent.lastElementChild.lastElementChild.clientHeight; - const wrapperLeft = wrapperContent.offsetLeft; - const wrapperTop = wrapperContent.offsetTop; + const wrapperLeft = wrapperContent.getBoundingClientRect().left; + const wrapperTop = wrapperContent.getBoundingClientRect().top; expect(wrapperTop).toEqual(expectedTop); expect(wrapperLeft).toEqual(expectedLeft); })); @@ -1494,48 +1544,61 @@ describe('igxOverlay', () => { const fix = TestBed.createComponent(EmptyPageComponent); fix.detectChanges(); const button = fix.componentInstance.buttonElement.nativeElement; - const positionSettings: PositionSettings = { - target: button - }; - const overlaySettings: OverlaySettings = { - positionStrategy: new AutoPositionStrategy(positionSettings), - modal: false, - closeOnOutsideClick: false - }; + 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]))); - vAlignmentArray.forEach(function (vAlignment) { - verifyOverlayBoundingSizeAndPosition(HorizontalAlignment.Left, VerticalAlignment.Bottom, - HorizontalAlignment.Right, VerticalAlignment[vAlignment]); - hAlignmentArray.forEach(function (hAlignment) { - verifyOverlayBoundingSizeAndPosition(HorizontalAlignment.Right, VerticalAlignment.Bottom, - HorizontalAlignment[hAlignment], VerticalAlignment[vAlignment]); + hAlignmentArray.forEach(function (horizontalStartPoint) { + vAlignmentArray.forEach(function (verticalStartPoint) { + hAlignmentArray.forEach(function (horizontalDirection) { + // do not check Center as we do nothing here + if (horizontalDirection === 'Center') { return; } + vAlignmentArray.forEach(function (verticalDirection) { + // do not check Middle as we do nothing here + if (verticalDirection === 'Middle') { return; } + const positionSettings: PositionSettings = { + target: button + }; + positionSettings.horizontalStartPoint = HorizontalAlignment[horizontalStartPoint]; + positionSettings.verticalStartPoint = VerticalAlignment[verticalStartPoint]; + positionSettings.horizontalDirection = HorizontalAlignment[horizontalDirection]; + positionSettings.verticalDirection = VerticalAlignment[verticalDirection]; + + const overlaySettings: OverlaySettings = { + positionStrategy: new AutoPositionStrategy(positionSettings), + modal: false, + closeOnOutsideClick: false + }; + + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + fix.detectChanges(); + + const targetRect: ClientRect = (positionSettings.target).getBoundingClientRect() as ClientRect; + const overlayWrapperElement = document.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0]; + const overlayWrapperRect: ClientRect = overlayWrapperElement.getBoundingClientRect() as ClientRect; + const screenRect: ClientRect = { + left: 0, + top: 0, + right: window.innerWidth, + bottom: window.innerHeight, + width: window.innerWidth, + height: window.innerHeight, + }; + + const location = getOverlayWrapperLocation(positionSettings, targetRect, overlayWrapperRect, screenRect); + expect(overlayWrapperRect.top.toFixed(1)).toEqual(location.y.toFixed(1)); + expect(overlayWrapperRect.left.toFixed(1)).toEqual(location.x.toFixed(1)); + expect(document.body.scrollHeight > document.body.clientHeight).toBeFalsy(); // check scrollbar + fix.componentInstance.overlay.hideAll(); + tick(); + fix.detectChanges(); + }); + }); }); }); - - // TODO: refactor this function and use it in all tests when needed - function verifyOverlayBoundingSizeAndPosition(horizontalDirection, verticalDirection, - horizontalAlignment, verticalAlignment) { - positionSettings.horizontalDirection = horizontalDirection; - positionSettings.verticalDirection = verticalDirection; - positionSettings.horizontalStartPoint = horizontalAlignment; - positionSettings.verticalStartPoint = verticalAlignment; - overlaySettings.positionStrategy = new AutoPositionStrategy(positionSettings); - fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); - tick(); - const buttonRect = button.getBoundingClientRect(); - const overlayElement = document.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0]; - const overlayRect = overlayElement.getBoundingClientRect(); - const expectedTop = getExpectedTopPosition(verticalAlignment, buttonRect); - const expectedLeft = horizontalDirection === HorizontalAlignment.Left ? - buttonRect.right - overlayRect.width : - getExpectedLeftPosition(horizontalAlignment, buttonRect); - expect(overlayRect.top.toFixed(1)).toEqual(expectedTop.toFixed(1)); - expect(overlayRect.bottom.toFixed(1)).toEqual((overlayRect.top + overlayRect.height).toFixed(1)); - expect(overlayRect.left.toFixed(1)).toEqual(expectedLeft.toFixed(1)); - expect(overlayRect.right.toFixed(1)).toEqual((overlayRect.left + overlayRect.width).toFixed(1)); - fix.componentInstance.overlay.hideAll(); - } })); it(`Should reposition the component and render it correctly in the window, even when the rendering options passed @@ -1544,64 +1607,69 @@ describe('igxOverlay', () => { const fix = TestBed.createComponent(EmptyPageComponent); fix.detectChanges(); const button = fix.componentInstance.buttonElement.nativeElement; - const positionSettings: PositionSettings = { - target: button - }; - const overlaySettings: OverlaySettings = { - positionStrategy: new AutoPositionStrategy(positionSettings), - modal: false, - closeOnOutsideClick: false - }; + 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]))); - hAlignmentArray.forEach(function (hAlignment) { - vAlignmentArray.forEach(function (vAlignment) { - if (hAlignment === 'Center') { - verifyOverlayBoundingSizeAndPosition(HorizontalAlignment.Left, VerticalAlignment.Bottom, - HorizontalAlignment.Center, VerticalAlignment[vAlignment]); - } - if (vAlignment !== 'Top') { - verifyOverlayBoundingSizeAndPosition(HorizontalAlignment.Right, VerticalAlignment.Top, - HorizontalAlignment[hAlignment], VerticalAlignment[vAlignment]); - if (hAlignment !== 'Left') { - verifyOverlayBoundingSizeAndPosition(HorizontalAlignment.Left, VerticalAlignment.Top, - HorizontalAlignment[hAlignment], VerticalAlignment[vAlignment]); + for (const buttonLocation of buttonLocations) { + for (const horizontalStartPoint of hAlignmentArray) { + for (const verticalStartPoint of vAlignmentArray) { + for (const horizontalDirection of hAlignmentArray) { + if (horizontalDirection === 'Center') { continue; } + for (const verticalDirection of vAlignmentArray) { + if (verticalDirection === 'Middle') { continue; } + + const positionSettings: PositionSettings = { + target: button + }; + 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 = { + positionStrategy: new AutoPositionStrategy(positionSettings), + modal: false, + closeOnOutsideClick: false + }; + + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + fix.detectChanges(); + + const targetRect = (positionSettings.target).getBoundingClientRect() as ClientRect; + const overlayWrapperElement = document.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0]; + const overlayWrapperRect = overlayWrapperElement.getBoundingClientRect() as ClientRect; + const screenRect: ClientRect = { + left: 0, + top: 0, + right: window.innerWidth, + bottom: window.innerHeight, + width: window.innerWidth, + height: window.innerHeight, + }; + + const loc = getOverlayWrapperLocation(positionSettings, targetRect, overlayWrapperRect, screenRect); + expect(overlayWrapperRect.top.toFixed(1)).toEqual(loc.y.toFixed(1)); + expect(overlayWrapperRect.left.toFixed(1)).toEqual(loc.x.toFixed(1)); + expect(document.body.scrollHeight > document.body.clientHeight).toBeFalsy(); // check scrollbar + fix.componentInstance.overlay.hideAll(); + tick(); + fix.detectChanges(); + } } } - }); - }); - - // TODO: refactor this function and use it in all tests when needed - function verifyOverlayBoundingSizeAndPosition(horizontalDirection, verticalDirection, - horizontalAlignment, verticalAlignment) { - positionSettings.horizontalDirection = horizontalDirection; - positionSettings.verticalDirection = verticalDirection; - positionSettings.horizontalStartPoint = horizontalAlignment; - positionSettings.verticalStartPoint = verticalAlignment; - overlaySettings.positionStrategy = new AutoPositionStrategy(positionSettings); - fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); - tick(); - fix.detectChanges(); - const buttonRect = button.getBoundingClientRect(); - const overlayElement = document.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0]; - const overlayRect = overlayElement.getBoundingClientRect(); - const expectedTop = verticalDirection === VerticalAlignment.Top ? - buttonRect.top + buttonRect.height : - getExpectedTopPosition(verticalAlignment, buttonRect); - const expectedLeft = (horizontalDirection === HorizontalAlignment.Left && - verticalDirection === VerticalAlignment.Top && - horizontalAlignment === HorizontalAlignment.Right) ? - buttonRect.right - overlayRect.width : - (horizontalDirection === HorizontalAlignment.Right && - verticalDirection === VerticalAlignment.Top) ? - getExpectedLeftPosition(horizontalAlignment, buttonRect) : - buttonRect.right; - expect(overlayRect.top.toFixed(1)).toEqual(expectedTop.toFixed(1)); - expect(overlayRect.bottom.toFixed(1)).toEqual((overlayRect.top + overlayRect.height).toFixed(1)); - expect(overlayRect.left.toFixed(1)).toEqual(expectedLeft.toFixed(1)); - expect(overlayRect.right.toFixed(1)).toEqual((overlayRect.left + overlayRect.width).toFixed(1)); - expect(document.body.scrollHeight > document.body.clientHeight).toBeFalsy(); // check scrollbar - fix.componentInstance.overlay.hideAll(); + } } })); @@ -1736,37 +1804,37 @@ describe('igxOverlay', () => { it(`Should persist the component's open state when scrolling, when scrolling and noOP scroll strategy is used (expanded DropDown remains expanded).`, fakeAsync(() => { - // TO DO replace Spies with css class and/or getBoundingClientRect. - const fixture = TestBed.createComponent(EmptyPageComponent); - const scrollTolerance = 10; - const scrollStrategy = new BlockScrollStrategy(); - const overlay = fixture.componentInstance.overlay; - const overlaySettings: OverlaySettings = { - modal: false, - scrollStrategy: scrollStrategy, - positionStrategy: new GlobalPositionStrategy() - }; + // TO DO replace Spies with css class and/or getBoundingClientRect. + const fixture = TestBed.createComponent(EmptyPageComponent); + const scrollTolerance = 10; + const scrollStrategy = new BlockScrollStrategy(); + const overlay = fixture.componentInstance.overlay; + const overlaySettings: OverlaySettings = { + modal: false, + scrollStrategy: scrollStrategy, + positionStrategy: new GlobalPositionStrategy() + }; - spyOn(scrollStrategy, 'initialize').and.callThrough(); - spyOn(scrollStrategy, 'attach').and.callThrough(); - spyOn(scrollStrategy, 'detach').and.callThrough(); - spyOn(overlay, 'hide').and.callThrough(); + spyOn(scrollStrategy, 'initialize').and.callThrough(); + spyOn(scrollStrategy, 'attach').and.callThrough(); + spyOn(scrollStrategy, 'detach').and.callThrough(); + spyOn(overlay, 'hide').and.callThrough(); - const scrollSpy = spyOn(scrollStrategy, 'onScroll').and.callThrough(); + const scrollSpy = spyOn(scrollStrategy, 'onScroll').and.callThrough(); - overlay.show(SimpleDynamicComponent, overlaySettings); - tick(); - expect(scrollStrategy.initialize).toHaveBeenCalledTimes(1); - expect(scrollStrategy.attach).toHaveBeenCalledTimes(1); - expect(scrollStrategy.detach).toHaveBeenCalledTimes(0); - expect(overlay.hide).toHaveBeenCalledTimes(0); - document.documentElement.scrollTop += scrollTolerance; - document.dispatchEvent(new Event('scroll')); - tick(); - expect(scrollSpy).toHaveBeenCalledTimes(1); - expect(overlay.hide).toHaveBeenCalledTimes(0); - expect(scrollStrategy.detach).toHaveBeenCalledTimes(0); - })); + overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + expect(scrollStrategy.initialize).toHaveBeenCalledTimes(1); + expect(scrollStrategy.attach).toHaveBeenCalledTimes(1); + expect(scrollStrategy.detach).toHaveBeenCalledTimes(0); + expect(overlay.hide).toHaveBeenCalledTimes(0); + document.documentElement.scrollTop += scrollTolerance; + document.dispatchEvent(new Event('scroll')); + tick(); + expect(scrollSpy).toHaveBeenCalledTimes(1); + expect(overlay.hide).toHaveBeenCalledTimes(0); + expect(scrollStrategy.detach).toHaveBeenCalledTimes(0); + })); it('Should persist the component open state when scrolling and absolute scroll strategy is used.', fakeAsync(() => { // TO DO replace Spies with css class and/or getBoundingClientRect. @@ -1801,33 +1869,478 @@ describe('igxOverlay', () => { expect(overlay.hide).toHaveBeenCalledTimes(0); })); - // 3. Interaction - // 3.1 Modal - it('Should apply a greyed-out mask layers when is modal.', fakeAsync(() => { - const fixture = TestBed.createComponent(EmptyPageComponent); + // 1.4 ElasticPosition (resize shown component to fit into visible window) + it('Should correctly render igx-overlay', fakeAsync(() => { + const fix = TestBed.createComponent(EmptyPageComponent); + fix.detectChanges(); const overlaySettings: OverlaySettings = { - modal: true, + positionStrategy: new ElasticPositionStrategy(), + scrollStrategy: new NoOpScrollStrategy(), + modal: false, + closeOnOutsideClick: false }; - fixture.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); - const overlayWrapper = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER_MODAL)[0]; + const positionSettings: PositionSettings = { + horizontalDirection: HorizontalAlignment.Right, + verticalDirection: VerticalAlignment.Bottom, + target: fix.componentInstance.buttonElement.nativeElement, + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Top + }; + overlaySettings.positionStrategy = new ElasticPositionStrategy(positionSettings); + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); tick(); - const styles = css(overlayWrapper); - const expectedBackgroundColor = 'background-color: rgba(0, 0, 0, 0.38)'; - const appliedBackgroundStyles = styles[3]; - expect(appliedBackgroundStyles).toContain(expectedBackgroundColor); + fix.detectChanges(); + const wrapper = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; + expect(wrapper).toBeDefined(); + expect(wrapper.classList).toContain(CLASS_OVERLAY_WRAPPER); })); - it('Should allow interaction only for the shown component when is modal.', fakeAsync(() => { - - // Utility handler meant for later detachment - // TO DO replace Spies with css class and/or getBoundingClientRect. - function _handler(event) { - if (event.which === 1) { - fixture.detectChanges(); - tick(); - expect(button.click).toHaveBeenCalledTimes(0); - expect(button.onclick).toHaveBeenCalledTimes(0); + it('Should cover the whole window 100% width and height.', fakeAsync(() => { + const fix = TestBed.createComponent(EmptyPageComponent); + fix.detectChanges(); + const overlaySettings: OverlaySettings = { + positionStrategy: new GlobalPositionStrategy(), + scrollStrategy: new NoOpScrollStrategy(), + modal: false, + closeOnOutsideClick: false + }; + const positionSettings: PositionSettings = { + horizontalDirection: HorizontalAlignment.Right, + verticalDirection: VerticalAlignment.Bottom, + target: fix.componentInstance.buttonElement.nativeElement, + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Top + }; + overlaySettings.positionStrategy = new ElasticPositionStrategy(positionSettings); + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + fix.detectChanges(); + const wrapper = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; + expect(wrapper.clientHeight).toEqual(window.innerHeight); + expect(wrapper.clientWidth).toEqual(window.innerWidth); + })); + + it('Should append the shown component inside the igx-overlay as a last child.', fakeAsync(() => { + const fix = TestBed.createComponent(EmptyPageComponent); + fix.detectChanges(); + const overlaySettings: OverlaySettings = { + positionStrategy: new ElasticPositionStrategy(), + scrollStrategy: new NoOpScrollStrategy(), + modal: false, + closeOnOutsideClick: false + }; + const positionSettings: PositionSettings = { + horizontalDirection: HorizontalAlignment.Right, + verticalDirection: VerticalAlignment.Bottom, + target: fix.componentInstance.buttonElement.nativeElement, + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Top + }; + overlaySettings.positionStrategy = new ElasticPositionStrategy(positionSettings); + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + + fix.detectChanges(); + const wrappers = document.getElementsByClassName(CLASS_OVERLAY_CONTENT); + const wrapperContent = wrappers[wrappers.length - 1].lastElementChild; // wrapped in NG-COMPONENT + expect(wrapperContent.children.length).toEqual(1); + expect(wrapperContent.lastElementChild.getAttribute('style')) + .toEqual('position: absolute; width:100px; height: 100px; background-color: red'); + })); + + it('Should show the component inside of the viewport if it would normally be outside of bounds, BOTTOM + RIGHT.', fakeAsync(() => { + const fix = TestBed.createComponent(DownRightButtonComponent); + fix.detectChanges(); + + fix.componentInstance.positionStrategy = new ElasticPositionStrategy(); + const component = fix.componentInstance; + const buttonElement = fix.componentInstance.buttonElement.nativeElement; + component.ButtonPositioningSettings.horizontalDirection = HorizontalAlignment.Right; + component.ButtonPositioningSettings.verticalDirection = VerticalAlignment.Bottom; + component.ButtonPositioningSettings.verticalStartPoint = VerticalAlignment.Bottom; + component.ButtonPositioningSettings.horizontalStartPoint = HorizontalAlignment.Right; + component.ButtonPositioningSettings.target = buttonElement; + component.ButtonPositioningSettings.minSize = { width: 80, height: 80 }; + buttonElement.click(); + tick(); + fix.detectChanges(); + + const wrappers = document.getElementsByClassName(CLASS_OVERLAY_CONTENT); + const wrapperContent = wrappers[wrappers.length - 1] as HTMLElement; // wrapped in NG-COMPONENT + expect(wrapperContent.lastElementChild.clientWidth).toEqual(80); + expect(wrapperContent.lastElementChild.clientHeight).toEqual(80); + const expectedLeft = buttonElement.offsetLeft + buttonElement.offsetWidth; + const expectedTop = buttonElement.offsetTop + buttonElement.offsetHeight; + const wrapperLeft = wrapperContent.getBoundingClientRect().left; + const wrapperTop = wrapperContent.getBoundingClientRect().top; + expect(wrapperTop).toEqual(expectedTop); + expect(wrapperLeft).toEqual(expectedLeft); + })); + + it('Should display each shown component based on the options specified if the component fits into the visible window.', + fakeAsync(() => { + const fix = TestBed.createComponent(EmptyPageComponent); + fix.detectChanges(); + const button = fix.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(function (horizontalStartPoint) { + vAlignmentArray.forEach(function (verticalStartPoint) { + hAlignmentArray.forEach(function (horizontalDirection) { + // do not check Center as we do nothing here + if (horizontalDirection === 'Center') { return; } + vAlignmentArray.forEach(function (verticalDirection) { + // do not check Middle as we do nothing here + if (verticalDirection === 'Middle') { return; } + + const positionSettings: PositionSettings = { + target: button + }; + 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 = { + positionStrategy: new ElasticPositionStrategy(positionSettings), + modal: false, + closeOnOutsideClick: false + }; + + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + fix.detectChanges(); + + const targetRect: ClientRect = (positionSettings.target).getBoundingClientRect() as ClientRect; + const overlayWrapperElement = document.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0]; + const overlayWrapperRect: ClientRect = overlayWrapperElement.getBoundingClientRect() as ClientRect; + const screenRect: ClientRect = { + left: 0, + top: 0, + right: window.innerWidth, + bottom: window.innerHeight, + width: window.innerWidth, + height: window.innerHeight, + }; + + const location = + getOverlayWrapperLocation(positionSettings, targetRect, overlayWrapperRect, screenRect, true); + expect(overlayWrapperRect.top.toFixed(1)).toEqual(location.y.toFixed(1)); + expect(overlayWrapperRect.left.toFixed(1)).toEqual(location.x.toFixed(1)); + fix.componentInstance.overlay.hideAll(); + tick(); + fix.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 fix = TestBed.createComponent(EmptyPageComponent); + fix.detectChanges(); + const button = fix.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) { + if (horizontalDirection === 'Center') { continue; } + for (const verticalDirection of vAlignmentArray) { + if (verticalDirection === 'Middle') { continue; } + + const positionSettings: PositionSettings = { + target: button + }; + 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 = { + positionStrategy: new ElasticPositionStrategy(positionSettings), + modal: false, + closeOnOutsideClick: false + }; + + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + fix.detectChanges(); + + const targetRect = (positionSettings.target).getBoundingClientRect() as ClientRect; + const overlayWrapperElement = document.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0]; + const overlayWrapperRect = overlayWrapperElement.getBoundingClientRect() as ClientRect; + const screenRect: ClientRect = { + left: 0, + top: 0, + right: window.innerWidth, + bottom: window.innerHeight, + width: window.innerWidth, + height: window.innerHeight, + }; + + const loc = + getOverlayWrapperLocation(positionSettings, targetRect, overlayWrapperRect, screenRect, true); + expect(overlayWrapperRect.top.toFixed(1)).toEqual(loc.y.toFixed(1)); + expect(overlayWrapperRect.left.toFixed(1)).toEqual(loc.x.toFixed(1)); + expect(document.body.scrollHeight > document.body.clientHeight).toBeFalsy(); // check scrollbar + fix.componentInstance.overlay.hideAll(); + tick(); + fix.detectChanges(); + } + } + } + } + } + })); + + it('Should render margins correctly.', fakeAsync(() => { + const expectedMargin = '0px'; + const fix = TestBed.createComponent(EmptyPageComponent); + fix.detectChanges(); + const button = fix.componentInstance.buttonElement.nativeElement; + const positionSettings: PositionSettings = { + target: button + }; + const overlaySettings: OverlaySettings = { + positionStrategy: new ElasticPositionStrategy(positionSettings), + scrollStrategy: new NoOpScrollStrategy(), + modal: false, + closeOnOutsideClick: false + }; + const hAlignmentArray = Object.keys(HorizontalAlignment).filter(key => !isNaN(Number(HorizontalAlignment[key]))); + const vAlignmentArray = Object.keys(VerticalAlignment).filter(key => !isNaN(Number(VerticalAlignment[key]))); + + hAlignmentArray.forEach(function (hDirection) { + vAlignmentArray.forEach(function (vDirection) { + hAlignmentArray.forEach(function (hAlignment) { + vAlignmentArray.forEach(function (vAlignment) { + verifyOverlayMargins(hDirection, vDirection, hAlignment, vAlignment); + }); + }); + }); + }); + + function verifyOverlayMargins(horizontalDirection, verticalDirection, horizontalAlignment, verticalAlignment) { + positionSettings.horizontalDirection = horizontalDirection; + positionSettings.verticalDirection = verticalDirection; + positionSettings.horizontalStartPoint = horizontalAlignment; + positionSettings.verticalStartPoint = verticalAlignment; + overlaySettings.positionStrategy = new ElasticPositionStrategy(positionSettings); + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + fix.detectChanges(); + const overlayWrapper = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; + const overlayContent = document.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0]; + const overlayElement = overlayContent.children[0]; + const wrapperMargin = window.getComputedStyle(overlayWrapper, null).getPropertyValue('margin'); + const contentMargin = window.getComputedStyle(overlayContent, null).getPropertyValue('margin'); + const elementMargin = window.getComputedStyle(overlayElement, null).getPropertyValue('margin'); + expect(wrapperMargin).toEqual(expectedMargin); + expect(contentMargin).toEqual(expectedMargin); + expect(elementMargin).toEqual(expectedMargin); + fix.componentInstance.overlay.hideAll(); + } + })); + + // When adding more than one component to show in igx-overlay: + it('When the options used to fit the component in the window - adding a new instance of the component with the ' + + ' same options will render it on top of the previous one.', fakeAsync(() => { + const fix = TestBed.createComponent(EmptyPageComponent); + fix.detectChanges(); + const button = fix.componentInstance.buttonElement.nativeElement; + const positionSettings: PositionSettings = { + horizontalDirection: HorizontalAlignment.Right, + verticalDirection: VerticalAlignment.Bottom, + target: button, + horizontalStartPoint: HorizontalAlignment.Center, + verticalStartPoint: VerticalAlignment.Bottom + }; + const overlaySettings: OverlaySettings = { + positionStrategy: new ElasticPositionStrategy(positionSettings), + scrollStrategy: new NoOpScrollStrategy(), + modal: false, + closeOnOutsideClick: false + }; + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + fix.detectChanges(); + tick(); + + const buttonRect = button.getBoundingClientRect(); + const overlayWrapper_1 = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; + const componentEl_1 = overlayWrapper_1.children[0].children[0]; + const overlayWrapper_2 = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[1]; + const componentEl_2 = overlayWrapper_2.children[0].children[0]; + const componentRect_1 = componentEl_1.getBoundingClientRect(); + const componentRect_2 = componentEl_2.getBoundingClientRect(); + expect(componentRect_1.left.toFixed(1)).toEqual((buttonRect.left + buttonRect.width / 2).toFixed(1)); + expect(componentRect_1.left.toFixed(1)).toEqual(componentRect_2.left.toFixed(1)); + expect(componentRect_1.top.toFixed(1)).toEqual((buttonRect.top + buttonRect.height).toFixed(1)); + expect(componentRect_1.top.toFixed(1)).toEqual(componentRect_2.top.toFixed(1)); + expect(componentRect_1.width.toFixed(1)).toEqual(componentRect_2.width.toFixed(1)); + expect(componentRect_1.height.toFixed(1)).toEqual(componentRect_2.height.toFixed(1)); + })); + + // When adding more than one component to show in igx-overlay and the options used will not fit the component in the + // window, so element is resized. + it('When adding a new instance of the component with the same options, will render it on top of the previous one.', + fakeAsync(() => { + const fix = TestBed.createComponent(EmptyPageComponent); + fix.detectChanges(); + const button = fix.componentInstance.buttonElement.nativeElement; + const positionSettings: PositionSettings = { + horizontalDirection: HorizontalAlignment.Left, + verticalDirection: VerticalAlignment.Top, + target: button, + horizontalStartPoint: HorizontalAlignment.Left, + verticalStartPoint: VerticalAlignment.Top, + minSize: { width: 80, height: 80 } + }; + const overlaySettings: OverlaySettings = { + positionStrategy: new ElasticPositionStrategy(positionSettings), + scrollStrategy: new NoOpScrollStrategy(), + modal: false, + closeOnOutsideClick: false + }; + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + fix.detectChanges(); + tick(); + + fix.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + fix.detectChanges(); + tick(); + + const buttonRect = button.getBoundingClientRect(); + const overlayWrapper_1 = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[0]; + const componentEl_1 = overlayWrapper_1.children[0].children[0]; + const overlayWrapper_2 = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER)[1]; + const componentEl_2 = overlayWrapper_2.children[0].children[0]; + const componentRect_1 = componentEl_1.getBoundingClientRect(); + const componentRect_2 = componentEl_2.getBoundingClientRect(); + expect(componentRect_1.left).toEqual(buttonRect.left - positionSettings.minSize.width); + expect(componentRect_1.left).toEqual(componentRect_2.left); + expect(componentRect_1.top).toEqual(componentRect_2.top); + expect(componentRect_1.width).toEqual(componentRect_2.width); + expect(componentRect_1.height).toEqual(componentRect_2.height); + })); + + it(`Should persist the component's open state when scrolling, when scrolling and noOP scroll strategy is used + (expanded DropDown remains expanded).`, fakeAsync(() => { + // TO DO replace Spies with css class and/or getBoundingClientRect. + const fixture = TestBed.createComponent(EmptyPageComponent); + const scrollTolerance = 10; + const scrollStrategy = new BlockScrollStrategy(); + const overlay = fixture.componentInstance.overlay; + const overlaySettings: OverlaySettings = { + modal: false, + scrollStrategy: scrollStrategy, + positionStrategy: new ElasticPositionStrategy() + }; + + spyOn(scrollStrategy, 'initialize').and.callThrough(); + spyOn(scrollStrategy, 'attach').and.callThrough(); + spyOn(scrollStrategy, 'detach').and.callThrough(); + spyOn(overlay, 'hide').and.callThrough(); + + const scrollSpy = spyOn(scrollStrategy, 'onScroll').and.callThrough(); + + overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + expect(scrollStrategy.initialize).toHaveBeenCalledTimes(1); + expect(scrollStrategy.attach).toHaveBeenCalledTimes(1); + expect(scrollStrategy.detach).toHaveBeenCalledTimes(0); + expect(overlay.hide).toHaveBeenCalledTimes(0); + document.documentElement.scrollTop += scrollTolerance; + document.dispatchEvent(new Event('scroll')); + tick(); + expect(scrollSpy).toHaveBeenCalledTimes(1); + expect(overlay.hide).toHaveBeenCalledTimes(0); + expect(scrollStrategy.detach).toHaveBeenCalledTimes(0); + })); + + it('Should persist the component open state when scrolling and absolute scroll strategy is used.', fakeAsync(() => { + // TO DO replace Spies with css class and/or getBoundingClientRect. + const fixture = TestBed.createComponent(EmptyPageComponent); + const scrollTolerance = 10; + const scrollStrategy = new AbsoluteScrollStrategy(); + const overlay = fixture.componentInstance.overlay; + const overlaySettings: OverlaySettings = { + closeOnOutsideClick: false, + modal: false, + positionStrategy: new ElasticPositionStrategy(), + scrollStrategy: scrollStrategy + }; + + spyOn(scrollStrategy, 'initialize').and.callThrough(); + spyOn(scrollStrategy, 'attach').and.callThrough(); + spyOn(scrollStrategy, 'detach').and.callThrough(); + spyOn(overlay, 'hide').and.callThrough(); + + const scrollSpy = spyOn(scrollStrategy, 'onScroll').and.callThrough(); + + overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + expect(scrollStrategy.initialize).toHaveBeenCalledTimes(1); + expect(scrollStrategy.attach).toHaveBeenCalledTimes(1); + + document.documentElement.scrollTop += scrollTolerance; + document.dispatchEvent(new Event('scroll')); + tick(); + expect(scrollSpy).toHaveBeenCalledTimes(1); + expect(scrollStrategy.detach).toHaveBeenCalledTimes(0); + expect(overlay.hide).toHaveBeenCalledTimes(0); + })); + + // 3. Interaction + // 3.1 Modal + it('Should apply a greyed-out mask layers when is modal.', fakeAsync(() => { + const fixture = TestBed.createComponent(EmptyPageComponent); + const overlaySettings: OverlaySettings = { + modal: true, + }; + + fixture.componentInstance.overlay.show(SimpleDynamicComponent, overlaySettings); + const overlayWrapper = document.getElementsByClassName(CLASS_OVERLAY_WRAPPER_MODAL)[0]; + tick(); + const styles = css(overlayWrapper); + const expectedBackgroundColor = 'background-color: rgba(0, 0, 0, 0.38)'; + const appliedBackgroundStyles = styles[3]; + expect(appliedBackgroundStyles).toContain(expectedBackgroundColor); + })); + + it('Should allow interaction only for the shown component when is modal.', fakeAsync(() => { + + // Utility handler meant for later detachment + // TO DO replace Spies with css class and/or getBoundingClientRect. + function _handler(event) { + if (event.which === 1) { + fixture.detectChanges(); + tick(); + expect(button.click).toHaveBeenCalledTimes(0); + expect(button.onclick).toHaveBeenCalledTimes(0); document.removeEventListener('click', _handler); dummy.remove(); } @@ -2003,7 +2516,7 @@ describe('igxOverlay', () => { })); }); - describe('Integration tests p2 (overrides): ', () => { + describe('Integration tests - Scroll Strategies: ', () => { beforeEach(async(() => { TestBed.configureTestingModule({ imports: [IgxToggleModule, DynamicModule, NoopAnimationsModule], @@ -2012,16 +2525,16 @@ describe('igxOverlay', () => { })); // If adding a component near the visible window borders(left,right,up,down) // it should be partially hidden and based on scroll strategy: - it('Should not allow scrolling with scroll strategy is not passed.', fakeAsync( async () => { + it('Should not allow scrolling with scroll strategy is not passed.', fakeAsync(async () => { TestBed.overrideComponent(EmptyPageComponent, { set: { styles: [`button { - position: absolute; - top: 850px; - left: -30px; - width: 100px; - height: 60px; - }`] + position: absolute; + top: 850px; + left: -30px; + width: 100px; + height: 60px; + } `] } }); await TestBed.compileComponents(); @@ -2067,7 +2580,7 @@ describe('igxOverlay', () => { it('Should retain the component state when scrolling and block scroll strategy is used.', fakeAsync(async () => { TestBed.overrideComponent(EmptyPageComponent, { set: { - styles: [`button { position: absolute, bottom: -2000px; }`] + styles: [`button { position: absolute, bottom: -2000px; } `] } }); await TestBed.compileComponents(); @@ -2098,39 +2611,190 @@ describe('igxOverlay', () => { document.documentElement.scrollTop += 1000; document.dispatchEvent(new Event('scroll')); tick(); - expect(document.documentElement.scrollTop).toEqual(0); - expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); - scrollStrat.detach(); + expect(document.documentElement.scrollTop).toEqual(0); + expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); + scrollStrat.detach(); + })); + + it(`Should show the component, AutoPositionStrategy, inside of the viewport if it would normally be outside of bounds, + TOP + LEFT.`, fakeAsync(async () => { + TestBed.overrideComponent(DownRightButtonComponent, { + set: { + styles: [`button { + position: absolute; + top: 16px; + left: 16px; + width: 84px; + height: 84px; + padding: 0px; + margin: 0px; + border: 0px; + } `] + } + }); + await TestBed.compileComponents(); + const fix = TestBed.createComponent(DownRightButtonComponent); + fix.detectChanges(); + + fix.componentInstance.positionStrategy = new AutoPositionStrategy(); + UIInteractions.clearOverlay(); + fix.detectChanges(); + const currentElement = fix.componentInstance; + const buttonElement = fix.componentInstance.buttonElement.nativeElement; + fix.detectChanges(); + currentElement.ButtonPositioningSettings.horizontalDirection = HorizontalAlignment.Left; + currentElement.ButtonPositioningSettings.verticalDirection = VerticalAlignment.Top; + currentElement.ButtonPositioningSettings.verticalStartPoint = VerticalAlignment.Top; + currentElement.ButtonPositioningSettings.horizontalStartPoint = HorizontalAlignment.Left; + currentElement.ButtonPositioningSettings.target = buttonElement; + buttonElement.click(); + fix.detectChanges(); + tick(); + + fix.detectChanges(); + const wrappers = document.getElementsByClassName(CLASS_OVERLAY_CONTENT); + const wrapperContent = wrappers[wrappers.length - 1] as HTMLElement; + const expectedStyle = 'position: absolute; width:100px; height: 100px; background-color: red'; + expect(wrapperContent.lastElementChild.lastElementChild.getAttribute('style')).toEqual(expectedStyle); + const buttonLeft = buttonElement.offsetLeft; + const buttonTop = buttonElement.offsetTop; + const expectedLeft = buttonLeft + buttonElement.clientWidth; // To the right of the button + const expectedTop = buttonTop + buttonElement.clientHeight; // Bottom of the button + const wrapperLeft = wrapperContent.getBoundingClientRect().left; + const wrapperTop = wrapperContent.getBoundingClientRect().top; + expect(wrapperTop).toEqual(expectedTop); + expect(wrapperLeft).toEqual(expectedLeft); + })); + + it(`Should show the component, AutoPositionStrategy, inside of the viewport if it would normally be outside of bounds, + TOP + RIGHT.`, fakeAsync(async () => { + TestBed.overrideComponent(DownRightButtonComponent, { + set: { + styles: [`button { + position: absolute; + top: 16px; + right: 16px; + width: 84px; + height: 84px; + padding: 0px; + margin: 0px; + border: 0px; + } `] + } + }); + await TestBed.compileComponents(); + const fix = TestBed.createComponent(DownRightButtonComponent); + fix.detectChanges(); + + fix.componentInstance.positionStrategy = new AutoPositionStrategy(); + UIInteractions.clearOverlay(); + fix.detectChanges(); + const currentElement = fix.componentInstance; + const buttonElement = fix.componentInstance.buttonElement.nativeElement; + fix.detectChanges(); + currentElement.ButtonPositioningSettings.horizontalDirection = HorizontalAlignment.Left; + currentElement.ButtonPositioningSettings.verticalDirection = VerticalAlignment.Top; + currentElement.ButtonPositioningSettings.verticalStartPoint = VerticalAlignment.Top; + currentElement.ButtonPositioningSettings.horizontalStartPoint = HorizontalAlignment.Left; + currentElement.ButtonPositioningSettings.target = buttonElement; + buttonElement.click(); + fix.detectChanges(); + tick(); + + fix.detectChanges(); + const wrappers = document.getElementsByClassName(CLASS_OVERLAY_CONTENT); + const wrapperContent = wrappers[wrappers.length - 1] as HTMLElement; + const expectedStyle = 'position: absolute; width:100px; height: 100px; background-color: red'; + expect(wrapperContent.lastElementChild.lastElementChild.getAttribute('style')).toEqual(expectedStyle); + const buttonLeft = buttonElement.offsetLeft; + const buttonTop = buttonElement.offsetTop; + const expectedRight = buttonLeft; // To the left of the button + const expectedTop = buttonTop + buttonElement.clientHeight; // Bottom of the button + const wrapperRight = wrapperContent.getBoundingClientRect().right; + const wrapperTop = wrapperContent.getBoundingClientRect().top; + expect(wrapperTop).toEqual(expectedTop); + expect(wrapperRight).toEqual(expectedRight); + })); + + it(`Should show the component, AutoPositionStrategy, inside of the viewport if it would normally be outside of bounds, + TOP + RIGHT.`, fakeAsync(async () => { + TestBed.overrideComponent(DownRightButtonComponent, { + set: { + styles: [`button { + position: absolute; + top: 16px; + right: 16px; + width: 84px; + height: 84px; + padding: 0px; + margin: 0px; + border: 0px; + } `] + } + }); + await TestBed.compileComponents(); + const fix = TestBed.createComponent(DownRightButtonComponent); + fix.detectChanges(); + + fix.componentInstance.positionStrategy = new AutoPositionStrategy(); + UIInteractions.clearOverlay(); + fix.detectChanges(); + const currentElement = fix.componentInstance; + const buttonElement = fix.componentInstance.buttonElement.nativeElement; + fix.detectChanges(); + currentElement.ButtonPositioningSettings.horizontalDirection = HorizontalAlignment.Right; + currentElement.ButtonPositioningSettings.verticalDirection = VerticalAlignment.Top; + currentElement.ButtonPositioningSettings.verticalStartPoint = VerticalAlignment.Top; + currentElement.ButtonPositioningSettings.horizontalStartPoint = HorizontalAlignment.Right; + currentElement.ButtonPositioningSettings.target = buttonElement; + buttonElement.click(); + fix.detectChanges(); + tick(); + + fix.detectChanges(); + const wrappers = document.getElementsByClassName(CLASS_OVERLAY_CONTENT); + const wrapperContent = wrappers[wrappers.length - 1] as HTMLElement; // wrapper in NG-COMPONENT + const expectedStyle = 'position: absolute; width:100px; height: 100px; background-color: red'; + expect(wrapperContent.lastElementChild.lastElementChild.getAttribute('style')).toEqual(expectedStyle); + const buttonLeft = buttonElement.offsetLeft; + const buttonTop = buttonElement.offsetTop; + const expectedLeft = buttonLeft - wrapperContent.lastElementChild.lastElementChild.clientWidth; // To the left of the button + const expectedTop = buttonTop + buttonElement.clientHeight; // Bottom of the button + const wrapperLeft = wrapperContent.getBoundingClientRect().left; + const wrapperTop = wrapperContent.getBoundingClientRect().top; + expect(wrapperTop).toEqual(expectedTop); + expect(wrapperLeft).toEqual(expectedLeft); })); - it('Should show the component inside of the viewport if it would normally be outside of bounds, TOP + LEFT.', - fakeAsync(async () => { + it(`Should show the component, AutoPositionStrategy, inside of the viewport if it would normally be outside of bounds, + BOTTOM + LEFT.`, fakeAsync(async () => { TestBed.overrideComponent(DownRightButtonComponent, { set: { styles: [`button { - position: absolute; - top: 16px; - left: 16px; - width: 84px; - height: 84px; - padding: 0px; - margin: 0px; - border: 0px; - }`] + position: absolute; + bottom: 16px; + left: 16px; + width: 84px; + height: 84px; + padding: 0px; + margin: 0px; + border: 0px; + } `] } }); await TestBed.compileComponents(); const fix = TestBed.createComponent(DownRightButtonComponent); fix.detectChanges(); + fix.componentInstance.positionStrategy = new AutoPositionStrategy(); UIInteractions.clearOverlay(); fix.detectChanges(); const currentElement = fix.componentInstance; const buttonElement = fix.componentInstance.buttonElement.nativeElement; fix.detectChanges(); currentElement.ButtonPositioningSettings.horizontalDirection = HorizontalAlignment.Left; - currentElement.ButtonPositioningSettings.verticalDirection = VerticalAlignment.Top; - currentElement.ButtonPositioningSettings.verticalStartPoint = VerticalAlignment.Top; + currentElement.ButtonPositioningSettings.verticalDirection = VerticalAlignment.Bottom; + currentElement.ButtonPositioningSettings.verticalStartPoint = VerticalAlignment.Bottom; currentElement.ButtonPositioningSettings.horizontalStartPoint = HorizontalAlignment.Left; currentElement.ButtonPositioningSettings.target = buttonElement; buttonElement.click(); @@ -2145,32 +2809,78 @@ describe('igxOverlay', () => { const buttonLeft = buttonElement.offsetLeft; const buttonTop = buttonElement.offsetTop; const expectedLeft = buttonLeft + buttonElement.clientWidth; // To the right of the button - const expectedTop = buttonTop + buttonElement.clientHeight; // Bottom of the button - const wrapperLeft = wrapperContent.offsetLeft; - const wrapperTop = wrapperContent.offsetTop; + const expectedTop = buttonTop - wrapperContent.lastElementChild.clientHeight; // On top of the button + const wrapperLeft = wrapperContent.getBoundingClientRect().left; + const wrapperTop = wrapperContent.getBoundingClientRect().top; expect(wrapperTop).toEqual(expectedTop); expect(wrapperLeft).toEqual(expectedLeft); })); - it('Should show the component inside of the viewport if it would normally be outside of bounds, TOP + RIGHT.', - fakeAsync(async () => { + it(`Should show the component, ElasticPositionStrategy, inside of the viewport if it would normally be outside of bounds, + TOP + LEFT.`, fakeAsync(async () => { TestBed.overrideComponent(DownRightButtonComponent, { set: { styles: [`button { - position: absolute; - top: 16px; - right: 16px; - width: 84px; - height: 84px; - padding: 0px; - margin: 0px; - border: 0px; - }`] + position: absolute; + top: 16px; + left: 16px; + width: 84px; + height: 84px; + padding: 0px; + margin: 0px; + border: 0px; + } `] + } + }); + await TestBed.compileComponents(); + const fix = TestBed.createComponent(DownRightButtonComponent); + fix.detectChanges(); + + fix.componentInstance.positionStrategy = new ElasticPositionStrategy(); + UIInteractions.clearOverlay(); + fix.detectChanges(); + const currentElement = fix.componentInstance; + const buttonElement = fix.componentInstance.buttonElement.nativeElement; + currentElement.ButtonPositioningSettings.horizontalDirection = HorizontalAlignment.Left; + currentElement.ButtonPositioningSettings.verticalDirection = VerticalAlignment.Top; + currentElement.ButtonPositioningSettings.verticalStartPoint = VerticalAlignment.Top; + currentElement.ButtonPositioningSettings.horizontalStartPoint = HorizontalAlignment.Left; + currentElement.ButtonPositioningSettings.target = buttonElement; + currentElement.ButtonPositioningSettings.minSize = { width: 80, height: 80 }; + buttonElement.click(); + tick(); + fix.detectChanges(); + + const wrappers = document.getElementsByClassName(CLASS_OVERLAY_CONTENT); + const wrapperContent = wrappers[wrappers.length - 1]; // wrapper in NG-COMPONENT + const expectedLeft = buttonElement.offsetLeft - currentElement.ButtonPositioningSettings.minSize.width; + const expectedTop = buttonElement.offsetTop - currentElement.ButtonPositioningSettings.minSize.height; + const componentRect = wrapperContent.lastElementChild.getBoundingClientRect(); + expect(componentRect.left).toEqual(expectedLeft); + expect(componentRect.top).toEqual(expectedTop); + })); + + it(`Should show the component, ElasticPositionStrategy, inside of the viewport if it would normally be outside of bounds, + TOP + RIGHT.`, fakeAsync(async () => { + TestBed.overrideComponent(DownRightButtonComponent, { + set: { + styles: [`button { + position: absolute; + top: 16px; + right: 16px; + width: 84px; + height: 84px; + padding: 0px; + margin: 0px; + border: 0px; + } `] } }); await TestBed.compileComponents(); const fix = TestBed.createComponent(DownRightButtonComponent); fix.detectChanges(); + + fix.componentInstance.positionStrategy = new ElasticPositionStrategy(); UIInteractions.clearOverlay(); fix.detectChanges(); const currentElement = fix.componentInstance; @@ -2181,44 +2891,42 @@ describe('igxOverlay', () => { currentElement.ButtonPositioningSettings.verticalStartPoint = VerticalAlignment.Top; currentElement.ButtonPositioningSettings.horizontalStartPoint = HorizontalAlignment.Right; currentElement.ButtonPositioningSettings.target = buttonElement; + currentElement.ButtonPositioningSettings.minSize = { width: 80, height: 80 }; buttonElement.click(); fix.detectChanges(); tick(); - fix.detectChanges(); + const wrappers = document.getElementsByClassName(CLASS_OVERLAY_CONTENT); - const wrapperContent = wrappers[wrappers.length - 1] as HTMLElement; // wrapper in NG-COMPONENT - const expectedStyle = 'position: absolute; width:100px; height: 100px; background-color: red'; - expect(wrapperContent.lastElementChild.lastElementChild.getAttribute('style')).toEqual(expectedStyle); - const buttonLeft = buttonElement.offsetLeft; - const buttonTop = buttonElement.offsetTop; - const expectedLeft = buttonLeft - wrapperContent.lastElementChild.lastElementChild.clientWidth; // To the left of the button - const expectedTop = buttonTop + buttonElement.clientHeight; // Bottom of the button - const wrapperLeft = wrapperContent.offsetLeft; - const wrapperTop = wrapperContent.offsetTop; - expect(wrapperTop).toEqual(expectedTop); - expect(wrapperLeft).toEqual(expectedLeft); + const wrapperContent = wrappers[wrappers.length - 1]; // wrapper in NG-COMPONENT + const expectedLeft = buttonElement.offsetLeft + buttonElement.clientWidth; + const expectedTop = buttonElement.offsetTop - currentElement.ButtonPositioningSettings.minSize.height; + const componentRect = wrapperContent.lastElementChild.getBoundingClientRect(); + expect(componentRect.left).toEqual(expectedLeft); + expect(componentRect.top).toEqual(expectedTop); })); - it('Should show the component inside of the viewport if it would normally be outside of bounds, BOTTOM + LEFT.', - fakeAsync(async () => { + it(`Should show the component, ElasticPositionStrategy, inside of the viewport if it would normally be outside of bounds, + BOTTOM + LEFT.`, fakeAsync(async () => { TestBed.overrideComponent(DownRightButtonComponent, { set: { styles: [`button { - position: absolute; - bottom: 16px; - left: 16px; - width: 84px; - height: 84px; - padding: 0px; - margin: 0px; - border: 0px; - }`] + position: absolute; + bottom: 16px; + left: 16px; + width: 84px; + height: 84px; + padding: 0px; + margin: 0px; + border: 0px; + } `] } }); await TestBed.compileComponents(); const fix = TestBed.createComponent(DownRightButtonComponent); fix.detectChanges(); + + fix.componentInstance.positionStrategy = new ElasticPositionStrategy(); UIInteractions.clearOverlay(); fix.detectChanges(); const currentElement = fix.componentInstance; @@ -2229,35 +2937,31 @@ describe('igxOverlay', () => { currentElement.ButtonPositioningSettings.verticalStartPoint = VerticalAlignment.Bottom; currentElement.ButtonPositioningSettings.horizontalStartPoint = HorizontalAlignment.Left; currentElement.ButtonPositioningSettings.target = buttonElement; + currentElement.ButtonPositioningSettings.minSize = { width: 80, height: 80 }; buttonElement.click(); fix.detectChanges(); tick(); - fix.detectChanges(); + const wrappers = document.getElementsByClassName(CLASS_OVERLAY_CONTENT); - const wrapperContent = wrappers[wrappers.length - 1] as HTMLElement; - const expectedStyle = 'position: absolute; width:100px; height: 100px; background-color: red'; - expect(wrapperContent.lastElementChild.lastElementChild.getAttribute('style')).toEqual(expectedStyle); - const buttonLeft = buttonElement.offsetLeft; - const buttonTop = buttonElement.offsetTop; - const expectedLeft = buttonLeft + buttonElement.clientWidth; // To the right of the button - const expectedTop = buttonTop - wrapperContent.lastElementChild.clientHeight; // On top of the button - const wrapperLeft = wrapperContent.offsetLeft; - const wrapperTop = wrapperContent.offsetTop; - expect(wrapperTop).toEqual(expectedTop); - expect(wrapperLeft).toEqual(expectedLeft); + const wrapperContent = wrappers[wrappers.length - 1]; // wrapper in NG-COMPONENT + const expectedLeft = buttonElement.offsetLeft - currentElement.ButtonPositioningSettings.minSize.width; + const expectedTop = buttonElement.offsetTop + buttonElement.offsetHeight; + const componentRect = wrapperContent.lastElementChild.getBoundingClientRect(); + expect(componentRect.left).toEqual(expectedLeft); + expect(componentRect.top).toEqual(expectedTop); })); - // 2. Scroll Strategy (test with GlobalPositionStrategy(default)) + // 2. Scroll Strategy (test with GlobalPositionStrategy(default)) // 2.1. Scroll Strategy - None it('Should not scroll component, nor the window when none scroll strategy is passed. No scrolling happens.', fakeAsync(async () => { TestBed.overrideComponent(EmptyPageComponent, { set: { styles: [`button { - position: absolute; - top: 120%; - left:120%; - }`] + position: absolute; + top: 120%; + left: 120%; + } `] } }); await TestBed.compileComponents(); @@ -2287,124 +2991,89 @@ describe('igxOverlay', () => { it(`Should not close the shown component when none scroll strategy is passed. (example: expanded DropDown stays expanded during a scrolling attempt.)`, - fakeAsync(async () => { - TestBed.overrideComponent(EmptyPageComponent, { - set: { - styles: [`button { - position: absolute; - top: 120%; - left:120%; - }`] - } - }); - await TestBed.compileComponents(); - const fixture = TestBed.createComponent(EmptyPageComponent); - fixture.detectChanges(); + fakeAsync(async () => { + TestBed.overrideComponent(EmptyPageComponent, { + set: { + styles: [`button { + position: absolute; + top: 120%; + left: 120%; + } `] + } + }); + await TestBed.compileComponents(); + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); - const overlaySettings: OverlaySettings = { - modal: false, - }; - const overlay = fixture.componentInstance.overlay; + const overlaySettings: OverlaySettings = { + modal: false, + }; + const overlay = fixture.componentInstance.overlay; - overlay.show(SimpleDynamicComponent, overlaySettings); - tick(); - const contentWrapper = document.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0]; - const element = contentWrapper.firstChild as HTMLElement; - const elementRect = element.getBoundingClientRect(); + overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + const contentWrapper = document.getElementsByClassName(CLASS_OVERLAY_CONTENT)[0]; + const element = contentWrapper.firstChild as HTMLElement; + const elementRect = element.getBoundingClientRect(); - document.documentElement.scrollTop = 40; - document.documentElement.scrollLeft = 30; - document.dispatchEvent(new Event('scroll')); - tick(); + document.documentElement.scrollTop = 40; + document.documentElement.scrollLeft = 30; + document.dispatchEvent(new Event('scroll')); + tick(); - expect(elementRect).toEqual(element.getBoundingClientRect()); - expect(document.documentElement.scrollTop).toEqual(40); - expect(document.documentElement.scrollLeft).toEqual(30); - expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); - })); + expect(elementRect).toEqual(element.getBoundingClientRect()); + expect(document.documentElement.scrollTop).toEqual(40); + expect(document.documentElement.scrollLeft).toEqual(30); + expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); + })); // 2.2 Scroll Strategy - Closing. (Uses a tolerance and closes an expanded component upon scrolling if the tolerance is exceeded.) // (example: DropDown or Dialog component collapse/closes after scrolling 10px.) it('Should scroll until the set threshold is exceeded, and closing scroll strategy is used.', - fakeAsync(async () => { - TestBed.overrideComponent(EmptyPageComponent, { - set: { - styles: [ - 'button { position: absolute; top: 100%; left: 90% }' - ] - } - }); - await TestBed.compileComponents(); - const fixture = TestBed.createComponent(EmptyPageComponent); - fixture.detectChanges(); + fakeAsync(async () => { + TestBed.overrideComponent(EmptyPageComponent, { + set: { + styles: [ + 'button { position: absolute; top: 100%; left: 90% }' + ] + } + }); + await TestBed.compileComponents(); + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); - const scrollTolerance = 10; - const scrollStrategy = new CloseScrollStrategy(); - const overlay = fixture.componentInstance.overlay; - const overlaySettings: OverlaySettings = { - positionStrategy: new GlobalPositionStrategy(), - scrollStrategy: scrollStrategy, - modal: false - }; + const scrollTolerance = 10; + const scrollStrategy = new CloseScrollStrategy(); + const overlay = fixture.componentInstance.overlay; + const overlaySettings: OverlaySettings = { + positionStrategy: new GlobalPositionStrategy(), + scrollStrategy: scrollStrategy, + modal: false + }; - overlay.show(SimpleDynamicComponent, overlaySettings); - tick(); + overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); - document.documentElement.scrollTop = scrollTolerance; - document.dispatchEvent(new Event('scroll')); - tick(); - expect(document.documentElement.scrollTop).toEqual(scrollTolerance); - expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); + document.documentElement.scrollTop = scrollTolerance; + document.dispatchEvent(new Event('scroll')); + tick(); + expect(document.documentElement.scrollTop).toEqual(scrollTolerance); + expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); - document.documentElement.scrollTop = scrollTolerance * 2; - document.dispatchEvent(new Event('scroll')); - tick(); - expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(0); + document.documentElement.scrollTop = scrollTolerance * 2; + document.dispatchEvent(new Event('scroll')); + tick(); + expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(0); - })); + })); it(`Should not change the shown component shown state until it exceeds the scrolling tolerance set, - and closing scroll strategy is used.`, - fakeAsync(async () => { - TestBed.overrideComponent(EmptyPageComponent, { - set: { - styles: [ - 'button { position: absolute; top: 200%; left: 90% }' - ] - } - }); - await TestBed.compileComponents(); - const fixture = TestBed.createComponent(EmptyPageComponent); - fixture.detectChanges(); - - const scrollTolerance = 10; - const scrollStrategy = new CloseScrollStrategy(); - const overlay = fixture.componentInstance.overlay; - const overlaySettings: OverlaySettings = { - positionStrategy: new GlobalPositionStrategy(), - scrollStrategy: scrollStrategy, - closeOnOutsideClick: false, - modal: false - }; - - overlay.show(SimpleDynamicComponent, overlaySettings); - tick(); - expect(document.documentElement.scrollTop).toEqual(0); - - document.documentElement.scrollTop += scrollTolerance; - document.dispatchEvent(new Event('scroll')); - tick(); - expect(document.documentElement.scrollTop).toEqual(scrollTolerance); - expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); - fixture.destroy(); - })); - - it(`Should close the shown component shown when it exceeds the scrolling threshold set, and closing scroll strategy is used. - (an expanded DropDown, Menu, DatePicker, etc. collapses).`, fakeAsync(async () => { + and closing scroll strategy is used.`, + fakeAsync(async () => { TestBed.overrideComponent(EmptyPageComponent, { set: { styles: [ - 'button { position: absolute; top: 100%; left: 90% }' + 'button { position: absolute; top: 200%; left: 90% }' ] } }); @@ -2418,6 +3087,7 @@ describe('igxOverlay', () => { const overlaySettings: OverlaySettings = { positionStrategy: new GlobalPositionStrategy(), scrollStrategy: scrollStrategy, + closeOnOutsideClick: false, modal: false }; @@ -2428,15 +3098,49 @@ describe('igxOverlay', () => { document.documentElement.scrollTop += scrollTolerance; document.dispatchEvent(new Event('scroll')); tick(); - expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); expect(document.documentElement.scrollTop).toEqual(scrollTolerance); - - document.documentElement.scrollTop += scrollTolerance * 2; - document.dispatchEvent(new Event('scroll')); - tick(); - expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(0); + expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); + fixture.destroy(); })); + it(`Should close the shown component shown when it exceeds the scrolling threshold set, and closing scroll strategy is used. + (an expanded DropDown, Menu, DatePicker, etc.collapses).`, fakeAsync(async () => { + TestBed.overrideComponent(EmptyPageComponent, { + set: { + styles: [ + 'button { position: absolute; top: 100%; left: 90% }' + ] + } + }); + await TestBed.compileComponents(); + const fixture = TestBed.createComponent(EmptyPageComponent); + fixture.detectChanges(); + + const scrollTolerance = 10; + const scrollStrategy = new CloseScrollStrategy(); + const overlay = fixture.componentInstance.overlay; + const overlaySettings: OverlaySettings = { + positionStrategy: new GlobalPositionStrategy(), + scrollStrategy: scrollStrategy, + modal: false + }; + + overlay.show(SimpleDynamicComponent, overlaySettings); + tick(); + expect(document.documentElement.scrollTop).toEqual(0); + + document.documentElement.scrollTop += scrollTolerance; + document.dispatchEvent(new Event('scroll')); + tick(); + expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(1); + expect(document.documentElement.scrollTop).toEqual(scrollTolerance); + + document.documentElement.scrollTop += scrollTolerance * 2; + document.dispatchEvent(new Event('scroll')); + tick(); + expect(document.getElementsByClassName(CLASS_OVERLAY_WRAPPER).length).toEqual(0); + })); + // 2.3 Scroll Strategy - NoOp. it('Should retain the component static and only the background scrolls, when scrolling and noOP scroll strategy is used.', fakeAsync(async () => { @@ -2480,7 +3184,7 @@ describe('igxOverlay', () => { it('Should scroll everything except component when scrolling and absolute scroll strategy is used.', fakeAsync(async () => { // Should behave as NoOpScrollStrategy - TestBed.overrideComponent(EmptyPageComponent, { + TestBed.overrideComponent(EmptyPageComponent, { set: { styles: [ 'button { position: absolute; top:200%; left: 100%; }', @@ -2551,17 +3255,15 @@ describe('igxOverlay', () => { })); }); + describe('Integration tests p3 (IgniteUI components): ', () => { - beforeAll(() => { - TestBed.resetTestingModule(); - }); beforeEach(async(() => { TestBed.configureTestingModule({ imports: [IgxToggleModule, DynamicModule, NoopAnimationsModule, IgxComponentsModule], declarations: DIRECTIVE_COMPONENTS }).compileComponents(); })); - it(`Should properly be able to render components that have no initial content (IgxCalendar, IgxAvatar)`, fakeAsync(() => { + it(`Should properly be able to render components that have no initial content(IgxCalendar, IgxAvatar)`, fakeAsync(() => { const fixture = TestBed.createComponent(SimpleRefComponent); fixture.detectChanges(); const IGX_CALENDAR_CLASS = `.igx-calendar`; @@ -2601,7 +3303,7 @@ describe('igxOverlay', () => { }); @Component({ // tslint:disable-next-line:component-selector - selector: `simple-dynamic-component`, + selector: `simple - dynamic - component`, template: '
' }) export class SimpleDynamicComponent { @@ -2640,20 +3342,20 @@ export class SimpleBigSizeComponent { @Component({ template: ` -
-
-

AAAAA

-

AAAAA

-

AAAAA

-

AAAAA

-

AAAAA

-

AAAAA

-

AAAAA

-

AAAAA

-

AAAAA

+
+
+

AAAAA

+

AAAAA

+

AAAAA

+

AAAAA

+

AAAAA

+

AAAAA

+

AAAAA

+

AAAAA

+

AAAAA

-
` +
` }) export class SimpleDynamicWithDirectiveComponent { public visible = false; @@ -2690,7 +3392,7 @@ export class EmptyPageComponent { } @Component({ - template: ``, + template: ``, styles: [`button { position: absolute; bottom: 0px; @@ -2705,6 +3407,8 @@ export class EmptyPageComponent { export class DownRightButtonComponent { constructor(@Inject(IgxOverlayService) public overlay: IgxOverlayService) { } + public positionStrategy: IPositionStrategy; + @ViewChild('button') buttonElement: ElementRef; public ButtonPositioningSettings: PositionSettings = { @@ -2714,10 +3418,11 @@ export class DownRightButtonComponent { horizontalStartPoint: HorizontalAlignment.Left, verticalStartPoint: VerticalAlignment.Top }; + click(event) { - const positionStrategy = new AutoPositionStrategy(this.ButtonPositioningSettings); + this.positionStrategy.settings = this.ButtonPositioningSettings; this.overlay.show(SimpleDynamicComponent, { - positionStrategy: positionStrategy, + positionStrategy: this.positionStrategy, scrollStrategy: new NoOpScrollStrategy(), modal: false, closeOnOutsideClick: false @@ -2741,7 +3446,6 @@ export class TopLeftOffsetComponent { @ViewChild('button') buttonElement: ElementRef; click(event) { - const positionStrategy = new ConnectedPositioningStrategy(); this.overlay.show(SimpleDynamicComponent); } } diff --git a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts index 2a4ac31b4c3..298f2abcc51 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/overlay.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/overlay.ts @@ -166,7 +166,13 @@ export class IgxOverlayService implements OnDestroy { this.updateSize(info); this._overlayInfos.push(info); - settings.positionStrategy.position(info.elementRef.nativeElement.parentElement, info.initialSize, document, true); + info.originalElementStyle = info.elementRef.nativeElement.style; + settings.positionStrategy.position( + info.elementRef.nativeElement.parentElement, + { width: info.initialSize.width, height: info.initialSize.height }, + document, + true, + settings.positionStrategy.settings.minSize); settings.scrollStrategy.initialize(this._document, this, id); settings.scrollStrategy.attach(); } @@ -246,16 +252,18 @@ export class IgxOverlayService implements OnDestroy { * ``` */ reposition(id: string) { - const overlay = this.getOverlayById(id); - if (!overlay) { + const overlayInfo = this.getOverlayById(id); + if (!overlayInfo) { console.error('Wrong id provided in overlay.reposition method. Id: ' + id); return; } - overlay.settings.positionStrategy.position( - overlay.elementRef.nativeElement.parentElement, - overlay.initialSize, - this._document); + overlayInfo.settings.positionStrategy.position( + overlayInfo.elementRef.nativeElement.parentElement, + { width: overlayInfo.initialSize.width, height: overlayInfo.initialSize.height }, + this._document, + false, + overlayInfo.settings.positionStrategy.settings.minSize); } private getOverlayInfo(component: any): OverlayInfo { @@ -395,7 +403,7 @@ export class IgxOverlayService implements OnDestroy { this._overlayElement.parentElement.removeChild(this._overlayElement); this._overlayElement = null; } - + info.elementRef.nativeElement.style = info.originalElementStyle; this.onClosed.emit({ id: info.id, componentRef: info.componentRef }); } diff --git a/projects/igniteui-angular/src/lib/services/overlay/position/IPositionStrategy.ts b/projects/igniteui-angular/src/lib/services/overlay/position/IPositionStrategy.ts index 49807d9a695..520a81bbb60 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/position/IPositionStrategy.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/position/IPositionStrategy.ts @@ -16,9 +16,10 @@ export interface IPositionStrategy { * @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 minSize the size up to which element could be reduced * ```typescript * settings.positionStrategy.position(content, size, document, true); * ``` */ - position(contentElement: HTMLElement, size?: Size, document?: Document, initialCall?: boolean): void; + position(contentElement: HTMLElement, size?: Size, document?: Document, initialCall?: boolean, minSize?: Size): void; } diff --git a/projects/igniteui-angular/src/lib/services/overlay/position/README.md b/projects/igniteui-angular/src/lib/services/overlay/position/README.md index 6ea3d8b90fe..d6791fcbe75 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/position/README.md +++ b/projects/igniteui-angular/src/lib/services/overlay/position/README.md @@ -14,12 +14,18 @@ Position strategies determine where to display the component in the provided Igx |:----------------|:--------------------------|:-------------------------|:-------------------------|:-------------------------| | new Point(0, 0) | HorizontalAlignment.Right | VerticalAlignment.Bottom | HorizontalAlignment.Left | VerticalAlignment.Bottom | -3) **Auto** - Positions the element as in **Connected** positioning strategy and re-positions the element in the view port (calculating a different start point) in case the element is partially getting out of view, adding an offsetPadding. Defaults to: +3) **Auto** - Positions the element as in **Connected** positioning strategy and re-positions the element in the view port (calculating a different start point) in case the element is partially getting out of view. Defaults to: | target | horizontalDirection | verticalDirection | horizontalStartPoint | verticalStartPoint | |:----------------|:--------------------------|:-------------------------|:-------------------------|:-------------------------| | new Point(0, 0) | HorizontalAlignment.Right | VerticalAlignment.Bottom | HorizontalAlignment.Left | VerticalAlignment.Bottom | +4) **Elastic** - Positions the element as in **Connected** positioning strategy and resize the element to fit in the view port in case the element is partially getting out of view. Defaults to: + +| target | horizontalDirection | verticalDirection | horizontalStartPoint | verticalStartPoint | minSize | +|:----------------|:--------------------------|:-------------------------|:-------------------------|:-------------------------|-------------------------| +| 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. ```typescript @@ -28,7 +34,8 @@ const positionSettings: PositionSettings = { horizontalDirection: HorizontalAlignment.Right, verticalDirection: VerticalAlignment.Bottom, horizontalStartPoint: HorizontalAlignment.Left, - verticalStartPoint: VerticalAlignment.Bottom + verticalStartPoint: VerticalAlignment.Bottom, + minSize: { width: 100, height: 300 } }; const strategy = new ConnectedPositioningStrategy(positionSettings); @@ -48,11 +55,12 @@ import {AutoPositionStrategy, GlobalPositionStrategy, ConnectedPositioningStrate ## API ##### Methods -| Position Strategy | Name | Description | -|:------------------|:---------------------------------------------|:------------------------------------------------| -| Global | `position(contentElement)` | Positions the element, based on the horizontal and vertical directions. | -| 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.| +| Position Strategy | Name | Description | +|:------------------|:-------------------------------------------------------|:----------------------------------------------------------------------------------| +| Global | `position(contentElement)` | Positions the element, based on the horizontal and vertical directions. | +| 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.| ###### PositionSettings | Name | Type | Description | @@ -64,3 +72,4 @@ import {AutoPositionStrategy, GlobalPositionStrategy, ConnectedPositioningStrate |verticalStartPoint | VerticalAlignment | Target's starting point | |openAnimation | AnimationReferenceMetadata | Animation applied while overlay opens | |closeAnimation | AnimationReferenceMetadata | Animation applied while overlay closes | +|minSize | Size | The size up to which element could be reduced | diff --git a/projects/igniteui-angular/src/lib/services/overlay/position/auto-position-strategy.ts b/projects/igniteui-angular/src/lib/services/overlay/position/auto-position-strategy.ts index 1e1b68875d5..f778dae77d7 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/position/auto-position-strategy.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/position/auto-position-strategy.ts @@ -1,88 +1,35 @@ -import { PositionSettings, VerticalAlignment, HorizontalAlignment, Size } from './../utilities'; +import { VerticalAlignment, HorizontalAlignment, PositionSettings, Size } from './../utilities'; import { IPositionStrategy } from './IPositionStrategy'; -import { ConnectedPositioningStrategy } from './connected-positioning-strategy'; +import { BaseFitPositionStrategy } from './base-fit-position-strategy'; -enum Axis { - X = 1, - Y = 0 -} -export class AutoPositionStrategy extends ConnectedPositioningStrategy implements IPositionStrategy { - public offsetPadding = 16; - private _initialSettings; - - getViewPort(document) { // Material Design implementation - const clientRect = document.documentElement.getBoundingClientRect(); - const scrollPosition = { - top: -clientRect.top, - left: -clientRect.left - }; - const width = window.innerWidth; - const height = window.innerHeight; - - return { - top: scrollPosition.top, - left: scrollPosition.left, - bottom: scrollPosition.top + height, - right: scrollPosition.left + width, - height, - width - }; +export class AutoPositionStrategy extends BaseFitPositionStrategy implements IPositionStrategy { + fitHorizontal(element: HTMLElement, settings: PositionSettings, innerRect: ClientRect, outerRect: ClientRect, minSize: Size) { + switch (settings.horizontalDirection) { + case HorizontalAlignment.Left: + settings.horizontalDirection = HorizontalAlignment.Right; + settings.horizontalStartPoint = HorizontalAlignment.Right; + break; + case HorizontalAlignment.Right: + settings.horizontalDirection = HorizontalAlignment.Left; + settings.horizontalStartPoint = HorizontalAlignment.Left; + break; + } + super.position(element, this._initialSize); } - // The position method should return a
container that will host the component - /** @inheritdoc */ - position(contentElement: HTMLElement, size?: Size, document?: Document, initialCall?: boolean): void { - if (!initialCall) { - super.position(contentElement, size); - return; + fitVertical(element: HTMLElement, settings: PositionSettings, innerRect: ClientRect, outerRect: ClientRect, minSize: Size) { + switch (settings.verticalDirection) { + case VerticalAlignment.Top: + settings.verticalDirection = VerticalAlignment.Bottom; + settings.verticalStartPoint = VerticalAlignment.Bottom; + break; + case VerticalAlignment.Bottom: + settings.verticalDirection = VerticalAlignment.Top; + settings.verticalStartPoint = VerticalAlignment.Top; + break; } - this._initialSettings = this._initialSettings || Object.assign({}, this._initialSettings, this.settings); - this.settings = this._initialSettings ? Object.assign({}, this.settings, this._initialSettings) : this.settings; - const viewPort = this.getViewPort(document); - super.position(contentElement, size); - const checkIfMoveHorizontal = (elem: HTMLElement) => { - const leftBound = elem.offsetLeft; - const rightBound = elem.offsetLeft + elem.lastElementChild.clientWidth; - switch (this.settings.horizontalDirection) { - case HorizontalAlignment.Left: - if (leftBound < viewPort.left) { - this.settings.horizontalDirection = HorizontalAlignment.Right; - this.settings.horizontalStartPoint = HorizontalAlignment.Right; - } - break; - case HorizontalAlignment.Right: - if (rightBound > viewPort.right) { - this.settings.horizontalDirection = HorizontalAlignment.Left; - this.settings.horizontalStartPoint = HorizontalAlignment.Left; - } - break; - default: - return; - } - }; - const checkIfMoveVertical = (elem: HTMLElement) => { - const topBound = elem.offsetTop; - const bottomBound = elem.offsetTop + elem.lastElementChild.clientHeight; - switch (this.settings.verticalDirection) { - case VerticalAlignment.Top: - if (topBound < viewPort.top) { - this.settings.verticalDirection = VerticalAlignment.Bottom; - this.settings.verticalStartPoint = VerticalAlignment.Bottom; - } - break; - case VerticalAlignment.Bottom: - if (bottomBound > viewPort.bottom) { - this.settings.verticalDirection = VerticalAlignment.Top; - this.settings.verticalStartPoint = VerticalAlignment.Top; - } - break; - default: - return; - } - }; - checkIfMoveVertical(contentElement); - checkIfMoveHorizontal(contentElement); - super.position(contentElement, size); + + super.position(element, this._initialSize); } } diff --git a/projects/igniteui-angular/src/lib/services/overlay/position/base-fit-position-strategy.ts b/projects/igniteui-angular/src/lib/services/overlay/position/base-fit-position-strategy.ts new file mode 100644 index 00000000000..f24cee99912 --- /dev/null +++ b/projects/igniteui-angular/src/lib/services/overlay/position/base-fit-position-strategy.ts @@ -0,0 +1,71 @@ +import { ConnectedPositioningStrategy } from './connected-positioning-strategy'; +import { IPositionStrategy } from './IPositionStrategy'; +import { HorizontalAlignment, VerticalAlignment, PositionSettings, Size } from '../utilities'; + +export abstract class BaseFitPositionStrategy extends ConnectedPositioningStrategy implements IPositionStrategy { + protected _initialSettings: PositionSettings; + protected _initialSize: Size; + + position(contentElement: HTMLElement, size: Size, document?: Document, initialCall?: boolean, minSize?: Size): void { + this._initialSize = size; + super.position(contentElement, size); + if (!initialCall) { + return; + } + this._initialSettings = this._initialSettings || Object.assign({}, this._initialSettings, this.settings); + this.settings = this._initialSettings ? Object.assign({}, this.settings, this._initialSettings) : this.settings; + const elementRect: ClientRect = contentElement.getBoundingClientRect(); + const viewPort: ClientRect = { + left: 0, + top: 0, + right: window.innerWidth, + bottom: window.innerHeight, + width: window.innerWidth, + height: window.innerHeight, + }; + if (this.shouldFitHorizontal(this.settings, elementRect, viewPort)) { + this.fitHorizontal(contentElement, this.settings, elementRect, viewPort, minSize); + } + + if (this.shouldFitVertical(this.settings, elementRect, viewPort)) { + this.fitVertical(contentElement, this.settings, elementRect, viewPort, minSize); + } + } + + protected shouldFitHorizontal(settings: PositionSettings, innerRect: ClientRect, outerRect: ClientRect): boolean { + switch (settings.horizontalDirection) { + case HorizontalAlignment.Left: + if (innerRect.left < outerRect.left) { + return true; + } + break; + case HorizontalAlignment.Right: + if (innerRect.right > outerRect.right) { + return true; + } + break; + } + + return false; + } + + protected shouldFitVertical(settings: PositionSettings, innerRect: ClientRect, outerRect: ClientRect): boolean { + switch (settings.verticalDirection) { + case VerticalAlignment.Top: + if (innerRect.top < outerRect.top) { + return true; + } + break; + case VerticalAlignment.Bottom: + if (innerRect.bottom > outerRect.bottom) { + return true; + } + break; + } + + return false; + } + + abstract fitHorizontal(element: HTMLElement, settings: PositionSettings, innerRect: ClientRect, outerRect: ClientRect, minSize: Size); + abstract fitVertical(element: HTMLElement, settings: PositionSettings, innerRect: ClientRect, outerRect: ClientRect, minSize: Size); +} diff --git a/projects/igniteui-angular/src/lib/services/overlay/position/connected-positioning-strategy.ts b/projects/igniteui-angular/src/lib/services/overlay/position/connected-positioning-strategy.ts index cc5761ea3e6..cfed7991b76 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/position/connected-positioning-strategy.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/position/connected-positioning-strategy.ts @@ -11,7 +11,8 @@ export class ConnectedPositioningStrategy implements IPositionStrategy { horizontalStartPoint: HorizontalAlignment.Left, verticalStartPoint: VerticalAlignment.Bottom, openAnimation: scaleInVerTop, - closeAnimation: scaleOutVerTop + closeAnimation: scaleOutVerTop, + minSize: { width: 0, height: 0 } }; /** @inheritdoc */ @@ -21,13 +22,14 @@ export class ConnectedPositioningStrategy implements IPositionStrategy { this.settings = Object.assign({}, this._defaultSettings, settings); } - // we no longer use the element inside the position() as its dimensions are cached in rect - /** @inheritdoc */ - position(contentElement: HTMLElement, size?: Size, document?: Document, initialCall?: boolean): void { + position(contentElement: HTMLElement, size: Size, document?: Document, initialCall?: boolean, minSize?: Size): void { const startPoint = getPointFromPositionsSettings(this.settings, contentElement.parentElement); - contentElement.style.top = startPoint.y + this.settings.verticalDirection * size.height + 'px'; - contentElement.style.left = startPoint.x + this.settings.horizontalDirection * size.width + 'px'; + // TODO: extract transform setting in util function + let transformString = ''; + transformString += `translateX(${startPoint.x + this.settings.horizontalDirection * size.width}px) `; + transformString += `translateY(${startPoint.y + this.settings.verticalDirection * size.height}px)`; + contentElement.style.transform = transformString.trim(); } } diff --git a/projects/igniteui-angular/src/lib/services/overlay/position/elastic-position-strategy.ts b/projects/igniteui-angular/src/lib/services/overlay/position/elastic-position-strategy.ts new file mode 100644 index 00000000000..d598b3aa881 --- /dev/null +++ b/projects/igniteui-angular/src/lib/services/overlay/position/elastic-position-strategy.ts @@ -0,0 +1,53 @@ +import { IPositionStrategy } from './IPositionStrategy'; +import { BaseFitPositionStrategy } from './base-fit-position-strategy'; +import { Size, HorizontalAlignment, VerticalAlignment, PositionSettings } from '../utilities'; + +export class ElasticPositionStrategy extends BaseFitPositionStrategy implements IPositionStrategy { + fitHorizontal(element: HTMLElement, settings: PositionSettings, innerRect: ClientRect, outerRect: ClientRect, minSize: Size) { + switch (settings.horizontalDirection) { + case HorizontalAlignment.Left: { + let extend = outerRect.left - innerRect.left; + if (extend > innerRect.width - minSize.width) { + extend = innerRect.width - minSize.width; + } + const translateX = `translateX(${innerRect.left + extend}px)`; + element.style.transform = element.style.transform.replace(/translateX\([.-\d]+px\)/g, translateX); + (element.firstChild).style.width = `${innerRect.width - extend}px`; + break; + } + case HorizontalAlignment.Right: { + let extend = innerRect.right - outerRect.right; + if (extend > innerRect.width - minSize.width) { + extend = innerRect.width - minSize.width; + } + + (element.firstChild).style.width = `${innerRect.width - extend}px`; + break; + } + } + } + + fitVertical(element: HTMLElement, settings: PositionSettings, innerRect: ClientRect, outerRect: ClientRect, minSize: Size) { + switch (settings.verticalDirection) { + case VerticalAlignment.Top: { + let extend = outerRect.top - innerRect.top; + if (extend > innerRect.height - minSize.height) { + extend = innerRect.height - minSize.height; + } + const translateY = `translateY(${innerRect.top + extend}px)`; + element.style.transform = element.style.transform.replace(/translateY\([.-\d]+px\)/g, translateY); + (element.firstChild).style.height = `${innerRect.width - extend}px`; + break; + } + case VerticalAlignment.Bottom: { + let extend = innerRect.bottom - outerRect.bottom; + if (extend > innerRect.height - minSize.height) { + extend = innerRect.height - minSize.height; + } + + (element.firstChild).style.height = `${innerRect.height - extend}px`; + break; + } + } + } +} diff --git a/projects/igniteui-angular/src/lib/services/overlay/position/global-position-strategy.ts b/projects/igniteui-angular/src/lib/services/overlay/position/global-position-strategy.ts index dc17a6ff189..42cbd72edb8 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/position/global-position-strategy.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/position/global-position-strategy.ts @@ -1,5 +1,5 @@ import { IPositionStrategy } from './IPositionStrategy'; -import { PositionSettings, Point, HorizontalAlignment, VerticalAlignment, Size } from './../utilities'; +import { PositionSettings, HorizontalAlignment, VerticalAlignment, Size } from './../utilities'; import { fadeIn, fadeOut } from '../../../animations/main'; export class GlobalPositionStrategy implements IPositionStrategy { @@ -9,7 +9,8 @@ export class GlobalPositionStrategy implements IPositionStrategy { horizontalStartPoint: HorizontalAlignment.Center, verticalStartPoint: VerticalAlignment.Middle, openAnimation: fadeIn, - closeAnimation: fadeOut + closeAnimation: fadeOut, + minSize: { width: 0, height: 0 } }; /** @inheritdoc */ @@ -19,8 +20,7 @@ export class GlobalPositionStrategy implements IPositionStrategy { this.settings = Object.assign({}, this._defaultSettings, settings); } - /** @inheritdoc */ - position(contentElement: HTMLElement, size?: Size, document?: Document, initialCall?: boolean): void { + position(contentElement: HTMLElement, size?: Size, document?: Document, initialCall?: boolean, minSize?: Size): void { switch (this.settings.horizontalDirection) { case HorizontalAlignment.Left: contentElement.parentElement.style.justifyContent = 'flex-start'; diff --git a/projects/igniteui-angular/src/lib/services/overlay/position/index.ts b/projects/igniteui-angular/src/lib/services/overlay/position/index.ts index e419860412f..1c3dc6ae418 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/position/index.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/position/index.ts @@ -3,3 +3,4 @@ export * from './IPositionStrategy'; export * from './global-position-strategy'; export * from './connected-positioning-strategy'; export * from './auto-position-strategy'; +export * from './elastic-position-strategy'; diff --git a/projects/igniteui-angular/src/lib/services/overlay/utilities.ts b/projects/igniteui-angular/src/lib/services/overlay/utilities.ts index 3d6ad8d5b60..1cdab04f92b 100644 --- a/projects/igniteui-angular/src/lib/services/overlay/utilities.ts +++ b/projects/igniteui-angular/src/lib/services/overlay/utilities.ts @@ -38,6 +38,7 @@ export interface PositionSettings { openAnimation?: AnimationReferenceMetadata; /** Animation applied while overlay closes */ closeAnimation?: AnimationReferenceMetadata; + minSize?: Size; } export interface OverlaySettings { @@ -116,4 +117,5 @@ export interface OverlayInfo { closeAnimationPlayer?: AnimationPlayer; openAnimationInnerPlayer?: any; closeAnimationInnerPlayer?: any; + originalElementStyle?: string; } diff --git a/src/app/overlay/overlay.sample.ts b/src/app/overlay/overlay.sample.ts index 987fa7eb72a..3e95037c2cf 100644 --- a/src/app/overlay/overlay.sample.ts +++ b/src/app/overlay/overlay.sample.ts @@ -9,7 +9,8 @@ import { BlockScrollStrategy, CloseScrollStrategy, NoOpScrollStrategy, - IgxInputGroupModule + IgxInputGroupModule, + ElasticPositionStrategy } from 'igniteui-angular'; import { templateJitUrl } from '@angular/compiler'; @@ -49,7 +50,7 @@ export class OverlaySampleComponent { verticalStartPoints = ['Top', 'Middle', 'Bottom']; verticalStartPoint = 'Top'; - positionStrategies = ['Auto', 'Connected', 'Global']; + positionStrategies = ['Auto', 'Connected', 'Global', 'Elastic']; positionStrategy = 'Auto'; scrollStrategies = ['Absolute', 'Block', 'Close', 'NoOp']; @@ -155,6 +156,22 @@ export class OverlaySampleComponent { this.closeOnOutsideClick = true; this.modal = true; break; + case 'Elastic': + this._overlaySettings = { + positionStrategy: new ElasticPositionStrategy({ + minSize: { width: 50, height: 50 } + }), + scrollStrategy: new NoOpScrollStrategy(), + modal: true, + closeOnOutsideClick: true + }; + this.horizontalDirection = 'Right'; + this.verticalDirection = 'Bottom'; + this.horizontalStartPoint = 'Left'; + this.verticalStartPoint = 'Bottom'; + this.closeOnOutsideClick = true; + this.modal = true; + break; default: break; } @@ -182,35 +199,38 @@ export class OverlaySampleComponent { onChange2() { // WIP const stringMapping = { 'ScrollStrategy': { - 'Absolute' : new AbsoluteScrollStrategy(), - 'Block' : new BlockScrollStrategy(), - 'Close' : new CloseScrollStrategy(), - 'NoOp' : new NoOpScrollStrategy() + 'Absolute': new AbsoluteScrollStrategy(), + 'Block': new BlockScrollStrategy(), + 'Close': new CloseScrollStrategy(), + 'NoOp': new NoOpScrollStrategy() }, - 'PositionStrategy' : { - 'Auto' : new AutoPositionStrategy(), - 'Connected' : new ConnectedPositioningStrategy(), - 'Global' : new GlobalPositionStrategy() + 'PositionStrategy': { + 'Auto': new AutoPositionStrategy(), + 'Connected': new ConnectedPositioningStrategy(), + 'Global': new GlobalPositionStrategy(), + 'Elastic': new ElasticPositionStrategy({ + minSize: { width: 50, height: 50 } + }), }, - 'VerticalDirection' : { - 'Top' : -1, - 'Middle' : -0.5, - 'Bottom' : 0 + 'VerticalDirection': { + 'Top': -1, + 'Middle': -0.5, + 'Bottom': 0 }, - 'VerticalStartPoint' : { - 'Top' : -1, - 'Middle' : -0.5, - 'Bottom' : 0 + 'VerticalStartPoint': { + 'Top': -1, + 'Middle': -0.5, + 'Bottom': 0 }, - 'HorizontalDirection' : { - 'Left' : -1, - 'Center' : -0.5, - 'Right' : 0 + 'HorizontalDirection': { + 'Left': -1, + 'Center': -0.5, + 'Right': 0 }, - 'HorizontalStartPoint' : { - 'Left' : -1, - 'Center' : -0.5, - 'Right' : 0 + 'HorizontalStartPoint': { + 'Left': -1, + 'Center': -0.5, + 'Right': 0 } }; @@ -221,13 +241,13 @@ export class OverlaySampleComponent { closeOnOutsideClick: this.closeOnOutsideClick }; this._overlaySettings.positionStrategy.settings.verticalDirection = - stringMapping['VerticalDirection'][this.verticalDirection]; + stringMapping['VerticalDirection'][this.verticalDirection]; this._overlaySettings.positionStrategy.settings.verticalStartPoint = - stringMapping['VerticalStartPoint'][this.verticalStartPoint]; + stringMapping['VerticalStartPoint'][this.verticalStartPoint]; this._overlaySettings.positionStrategy.settings.horizontalDirection = - stringMapping['HorizontalDirection'][this.horizontalDirection]; + stringMapping['HorizontalDirection'][this.horizontalDirection]; this._overlaySettings.positionStrategy.settings.horizontalStartPoint = - stringMapping['HorizontalStartPoint'][this.horizontalStartPoint]; + stringMapping['HorizontalStartPoint'][this.horizontalStartPoint]; } onSwitchChange(ev) {