diff --git a/skyux-spa-visual-tests/screenshots-baseline/popover-Placement-Left-chrome-1000x800-dpr-1.png b/skyux-spa-visual-tests/screenshots-baseline/popover-Placement-Left-chrome-1000x800-dpr-1.png deleted file mode 100644 index ab9fbdae1..000000000 Binary files a/skyux-spa-visual-tests/screenshots-baseline/popover-Placement-Left-chrome-1000x800-dpr-1.png and /dev/null differ diff --git a/skyux-spa-visual-tests/screenshots-baseline/popover-Placement-Right-chrome-1000x800-dpr-1.png b/skyux-spa-visual-tests/screenshots-baseline/popover-Placement-Right-chrome-1000x800-dpr-1.png deleted file mode 100644 index 876e1b594..000000000 Binary files a/skyux-spa-visual-tests/screenshots-baseline/popover-Placement-Right-chrome-1000x800-dpr-1.png and /dev/null differ diff --git a/src/app/components/popover/index.html b/src/app/components/popover/index.html index 454b63dd6..b396645fb 100644 --- a/src/app/components/popover/index.html +++ b/src/app/components/popover/index.html @@ -12,6 +12,13 @@ isOptional="true"> Specifies the placement of the popover in relation to the trigger element (above, below, left, right). + + Specifies the user action that displays the popover (click, mouseenter). + diff --git a/src/app/components/popover/popover-demo.component.html b/src/app/components/popover/popover-demo.component.html index ba96b2f62..f95276998 100644 --- a/src/app/components/popover/popover-demo.component.html +++ b/src/app/components/popover/popover-demo.component.html @@ -110,6 +110,26 @@

The content of a popover can be text, HTML, or Angular components. +

+ Popovers can also be opened when the mouse hovers over an element: +

+ + + + + You can move your mouse off the button to close this popover. + +

Events

diff --git a/src/modules/popover/fixtures/popover.component.fixture.html b/src/modules/popover/fixtures/popover.component.fixture.html index c1722993a..172350a2e 100644 --- a/src/modules/popover/fixtures/popover.component.fixture.html +++ b/src/modules/popover/fixtures/popover.component.fixture.html @@ -6,6 +6,10 @@ Open + + Some text. diff --git a/src/modules/popover/index.ts b/src/modules/popover/index.ts index 7ed63d730..60c6b8cc3 100644 --- a/src/modules/popover/index.ts +++ b/src/modules/popover/index.ts @@ -1,5 +1,6 @@ export * from './popover-adapter.service'; export * from './popover-placement'; +export * from './popover-trigger'; export * from './popover.directive'; export * from './popover.component'; export * from './popover.module'; diff --git a/src/modules/popover/popover-adapter.service.spec.ts b/src/modules/popover/popover-adapter.service.spec.ts index f085f13d4..1d87a52e3 100644 --- a/src/modules/popover/popover-adapter.service.spec.ts +++ b/src/modules/popover/popover-adapter.service.spec.ts @@ -17,7 +17,6 @@ import { } from './popover-adapter.service'; describe('SkyPopoverAdapterService', () => { - let elementRefDefinition: any; let mockRenderer: any; class MockWindowService { @@ -38,18 +37,27 @@ describe('SkyPopoverAdapterService', () => { } } - beforeEach(() => { - elementRefDefinition = { + function createElementRefDefinition( + top = 0, + left = 0, + right = 0, + width = 0, + height = 0 + ) { + return { getBoundingClientRect: function () { return { - top: 0, - left: 0, - width: 0, - height: 0 + top: top, + left: left, + right: right, + width: width, + height: height }; } }; + } + beforeEach(() => { mockRenderer = { removeStyle() {}, setStyle() {}, @@ -68,59 +76,50 @@ describe('SkyPopoverAdapterService', () => { it('should set a popover\'s top and left coordinates', inject([SkyPopoverAdapterService], (adapterService: SkyPopoverAdapterService) => { - spyOn(adapterService['renderer'], 'setStyle'); + const spy = spyOn(adapterService['renderer'], 'setStyle'); + const popover = new ElementRef(createElementRefDefinition(0, 0, 0, 180, 70)); + const popoverArrow = new ElementRef(createElementRefDefinition(0, 0, 0, 20, 10)); adapterService.setPopoverPosition({ - popover: new ElementRef(elementRefDefinition), - popoverArrow: new ElementRef(elementRefDefinition), - caller: new ElementRef(elementRefDefinition) + popover, + popoverArrow, + caller: new ElementRef(createElementRefDefinition(200, 200, 0, 80, 34)) }, 'right'); - expect(adapterService['renderer'].setStyle).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith(popover.nativeElement, 'top', `182px`); + expect(spy).toHaveBeenCalledWith(popover.nativeElement, 'left', `0px`); + expect(spy).toHaveBeenCalledWith(popoverArrow.nativeElement, 'top', `35px`); + expect(spy).toHaveBeenCalledWith(popoverArrow.nativeElement, 'left', undefined); }) ); it('should default the placement to "above"', inject([SkyPopoverAdapterService], (adapterService: SkyPopoverAdapterService) => { - spyOn(adapterService['renderer'], 'setStyle'); + const spy = spyOn(adapterService as any, 'getCoordinates').and.callThrough(); + // Setting the caller's top value to 200 will make sure the popover has + // enough room to appear above. adapterService.setPopoverPosition({ - popover: new ElementRef(elementRefDefinition), - popoverArrow: new ElementRef(elementRefDefinition), - caller: new ElementRef(elementRefDefinition) + popover: new ElementRef(createElementRefDefinition(0, 0, 0, 276, 100)), + popoverArrow: new ElementRef(createElementRefDefinition()), + caller: new ElementRef(createElementRefDefinition(200, 0, 100, 100, 34)) }, undefined); - expect(adapterService['renderer'].setStyle).toHaveBeenCalled(); + expect(spy.calls.mostRecent().args[1]).toEqual('above'); }) ); it('should attempt to find the optimal placement if outside viewport', inject([SkyPopoverAdapterService], (adapterService: SkyPopoverAdapterService) => { - let newPlacement: string; - const subscription = adapterService.placementChanges.subscribe((data) => { - newPlacement = data; - }); + const spy = spyOn(adapterService as any, 'getCoordinates').and.callThrough(); adapterService.setPopoverPosition({ - popover: new ElementRef({ - getBoundingClientRect() { - return { top: 0, left: 0, width: 276, height: 100 }; - } - }), - popoverArrow: new ElementRef({ - getBoundingClientRect() { - return { top: 0, left: 0, width: 0, height: 0 }; - } - }), - caller: new ElementRef({ - getBoundingClientRect() { - return { top: 0, left: 0, right: 100, width: 100, height: 34 }; - } - }) + popover: new ElementRef(createElementRefDefinition(0, 0, 0, 276, 100)), + popoverArrow: new ElementRef(createElementRefDefinition()), + caller: new ElementRef(createElementRefDefinition(0, 0, 100, 100, 34)) }, 'above'); - subscription.unsubscribe(); - expect(newPlacement).toEqual('below'); + expect(spy.calls.mostRecent().args[1]).toEqual('below'); }) ); @@ -128,6 +127,11 @@ describe('SkyPopoverAdapterService', () => { inject( [SkyPopoverAdapterService, SkyWindowRefService], (adapterService: SkyPopoverAdapterService, windowService: SkyWindowRefService) => { + const spy = spyOn(adapterService as any, 'getCoordinates').and.callThrough(); + + // For this test, the window's dimensions have been set to be smaller than the popover. + // All cardinal directions should be checked (and fail), + // at which case the placement should be set to the opposite direction. spyOn(windowService, 'getWindow').and.returnValue({ setTimeout(callback: Function) { callback(); @@ -142,55 +146,42 @@ describe('SkyPopoverAdapterService', () => { pageYOffset: 0 }); - let newPlacement: string; - const subscription = adapterService.placementChanges.subscribe((data) => { - newPlacement = data; - }); + const elements = { + popover: new ElementRef(createElementRefDefinition(0, 0, 0, 276, 100)), + popoverArrow: new ElementRef(createElementRefDefinition()), + caller: new ElementRef(createElementRefDefinition(0, 0, 100, 100, 34)) + }; - adapterService.setPopoverPosition({ - popover: new ElementRef({ - getBoundingClientRect() { - return { top: 0, left: 0, width: 276, height: 100 }; - } - }), - popoverArrow: new ElementRef({ - getBoundingClientRect() { - return { top: 0, left: 0, width: 0, height: 0 }; - } - }), - caller: new ElementRef({ - getBoundingClientRect() { - return { top: 0, left: 0, right: 100, width: 100, height: 34 }; - } - }) - }, 'above'); + adapterService.setPopoverPosition(elements, 'below'); + expect(spy.calls.mostRecent().args[1]).toEqual('above'); - subscription.unsubscribe(); - // For this test, the window's dimensions have been set to be smaller than the popover. - // All cardinal directions should be checked (and fail), - // at which case the placement should be set to the opposite direction. - expect(newPlacement).toEqual('below'); + adapterService.setPopoverPosition(elements, 'right'); + expect(spy.calls.mostRecent().args[1]).toEqual('left'); + + adapterService.setPopoverPosition(elements, 'left'); + expect(spy.calls.mostRecent().args[1]).toEqual('right'); + + adapterService.setPopoverPosition(elements, 'above'); + expect(spy.calls.mostRecent().args[1]).toEqual('below'); } ) ); it('should hide a popover', inject([SkyPopoverAdapterService], (adapterService: SkyPopoverAdapterService) => { - spyOn(adapterService['renderer'], 'addClass'); + const spy = spyOn(adapterService['renderer'], 'addClass'); const elem = new ElementRef({ nativeElement: {} }); adapterService.hidePopover(elem); - expect(adapterService['renderer'].addClass) - .toHaveBeenCalledWith(elem.nativeElement, 'hidden'); + expect(spy).toHaveBeenCalledWith(elem.nativeElement, 'hidden'); }) ); it('should show a popover', inject([SkyPopoverAdapterService], (adapterService: SkyPopoverAdapterService) => { - spyOn(adapterService['renderer'], 'removeClass'); + const spy = spyOn(adapterService['renderer'], 'removeClass'); const elem = new ElementRef({ nativeElement: {} }); adapterService.showPopover(elem); - expect(adapterService['renderer'].removeClass) - .toHaveBeenCalledWith(elem.nativeElement, 'hidden'); + expect(spy).toHaveBeenCalledWith(elem.nativeElement, 'hidden'); }) ); }); diff --git a/src/modules/popover/popover-adapter.service.ts b/src/modules/popover/popover-adapter.service.ts index 98c019299..47e986040 100644 --- a/src/modules/popover/popover-adapter.service.ts +++ b/src/modules/popover/popover-adapter.service.ts @@ -4,9 +4,6 @@ import { Renderer2 } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; - import { SkyWindowRefService } from '../window'; import { SkyPopoverPlacement } from './index'; @@ -26,20 +23,13 @@ export interface SkyPopoverAdapterElements { @Injectable() export class SkyPopoverAdapterService { - public placementChanges: Observable; - private placementSubject: Subject; - constructor( private renderer: Renderer2, - private windowRef: SkyWindowRefService - ) { - this.placementSubject = new Subject(); - this.placementChanges = this.placementSubject.asObservable(); - } + private windowRef: SkyWindowRefService) { } public setPopoverPosition( elements: SkyPopoverAdapterElements, - placement: SkyPopoverPlacement + placement: SkyPopoverPlacement = 'above' ) { this.clearElementCoordinates(elements.popover); this.clearElementCoordinates(elements.popoverArrow); @@ -79,8 +69,6 @@ export class SkyPopoverAdapterService { coords = this.getCoordinates(elements, placement); } - this.sendChanges(placement); - return coords; } @@ -105,8 +93,9 @@ export class SkyPopoverAdapterService { let isOutsideViewport = false; + // tslint:disable:switch-default + // All possible types are represented; default unnecessary. switch (placement) { - default: case 'above': left = leftCenter; top = callerRect.top - popoverRect.height; @@ -131,6 +120,7 @@ export class SkyPopoverAdapterService { arrowTop = (popoverRect.height / 2); break; } + // tslint:enable:switch-default left += window.pageXOffset; top += window.pageYOffset; @@ -198,8 +188,19 @@ export class SkyPopoverAdapterService { } private setElementCoordinates(elem: ElementRef, top: number, left: number) { - this.renderer.setStyle(elem.nativeElement, 'top', `${top}px`); - this.renderer.setStyle(elem.nativeElement, 'left', `${left}px`); + let topStyle; + let leftStyle; + + if (top !== undefined) { + topStyle = `${top}px`; + } + + if (left !== undefined) { + leftStyle = `${left}px`; + } + + this.renderer.setStyle(elem.nativeElement, 'top', topStyle); + this.renderer.setStyle(elem.nativeElement, 'left', leftStyle); } private getNextPlacement(placement: SkyPopoverPlacement): SkyPopoverPlacement { @@ -217,8 +218,4 @@ export class SkyPopoverAdapterService { const pairings = { above: 'below', below: 'above', right: 'left', left: 'right' }; return pairings[placement] as SkyPopoverPlacement; } - - private sendChanges(placement: SkyPopoverPlacement) { - this.placementSubject.next(placement); - } } diff --git a/src/modules/popover/popover-trigger.ts b/src/modules/popover/popover-trigger.ts new file mode 100644 index 000000000..25c769aab --- /dev/null +++ b/src/modules/popover/popover-trigger.ts @@ -0,0 +1 @@ +export type SkyPopoverTrigger = 'click' | 'mouseenter'; diff --git a/src/modules/popover/popover.component.html b/src/modules/popover/popover.component.html index 0a86a3052..f3071f1e6 100644 --- a/src/modules/popover/popover.component.html +++ b/src/modules/popover/popover.component.html @@ -1,5 +1,5 @@