From e46a050537a26dd0f0f58416ca49dde4852cb22e Mon Sep 17 00:00:00 2001 From: Blackbaud-SteveBrush Date: Tue, 13 Feb 2018 19:57:15 -0500 Subject: [PATCH] Allow for async popover refs --- src/demos/popover/popover-demo.component.html | 15 ++ src/demos/popover/popover-demo.component.ts | 15 ++ .../fixtures/popover.component.fixture.html | 15 ++ .../fixtures/popover.component.fixture.ts | 20 ++- src/modules/popover/popover.directive.spec.ts | 55 +++++++ src/modules/popover/popover.directive.ts | 139 ++++++++++++------ 6 files changed, 212 insertions(+), 47 deletions(-) diff --git a/src/demos/popover/popover-demo.component.html b/src/demos/popover/popover-demo.component.html index 6a25bc21d..a551f2812 100644 --- a/src/demos/popover/popover-demo.component.html +++ b/src/demos/popover/popover-demo.component.html @@ -376,3 +376,18 @@

+ +

+ Popovers may also be asynchronous +

+ + + + + My asynchronous popover. + diff --git a/src/demos/popover/popover-demo.component.ts b/src/demos/popover/popover-demo.component.ts index 8cefc1369..21387836f 100644 --- a/src/demos/popover/popover-demo.component.ts +++ b/src/demos/popover/popover-demo.component.ts @@ -4,15 +4,30 @@ import { ViewChild } from '@angular/core'; +import { + SkyPopoverComponent +} from '../../modules/popover'; + @Component({ selector: 'sky-popover-demo', templateUrl: './popover-demo.component.html', styleUrls: ['./popover-demo.component.scss'] }) export class SkyPopoverDemoComponent { + public asyncPopoverRef: SkyPopoverComponent; + @ViewChild('remote') public remote: ElementRef; + @ViewChild('asyncPopover') + public asyncPopover: SkyPopoverComponent; + + constructor() { + setTimeout(() => { + this.asyncPopoverRef = this.asyncPopover; + }, 1000); + } + public onPopoverOpened(popoverComponent: any) { alert('The popover was opened: ' + popoverComponent.popoverTitle); } diff --git a/src/modules/popover/fixtures/popover.component.fixture.html b/src/modules/popover/fixtures/popover.component.fixture.html index 6494efc5e..893725e94 100644 --- a/src/modules/popover/fixtures/popover.component.fixture.html +++ b/src/modules/popover/fixtures/popover.component.fixture.html @@ -30,4 +30,19 @@ Some text. + + + + + My asynchronous popover. + + + + My asynchronous popover. + diff --git a/src/modules/popover/fixtures/popover.component.fixture.ts b/src/modules/popover/fixtures/popover.component.fixture.ts index 89fe1f3dc..ba358c418 100644 --- a/src/modules/popover/fixtures/popover.component.fixture.ts +++ b/src/modules/popover/fixtures/popover.component.fixture.ts @@ -1,10 +1,28 @@ import { - Component + Component, + ViewChild } from '@angular/core'; +import { SkyPopoverComponent } from '../popover.component'; + @Component({ selector: 'sky-test-component', templateUrl: './popover.component.fixture.html' }) export class SkyPopoverTestComponent { + public asyncPopoverRef: SkyPopoverComponent; + + @ViewChild('asyncPopover') + public asyncPopover: SkyPopoverComponent; + + @ViewChild('anotherAsyncPopover') + public anotherAsyncPopover: SkyPopoverComponent; + + public attachAsyncPopover() { + this.asyncPopoverRef = this.asyncPopover; + } + + public attachAnotherAsyncPopover() { + this.asyncPopoverRef = this.anotherAsyncPopover; + } } diff --git a/src/modules/popover/popover.directive.spec.ts b/src/modules/popover/popover.directive.spec.ts index 7e3fe1a68..13fc604e7 100644 --- a/src/modules/popover/popover.directive.spec.ts +++ b/src/modules/popover/popover.directive.spec.ts @@ -207,4 +207,59 @@ describe('SkyPopoverDirective', () => { TestUtility.fireKeyboardEvent(caller.nativeElement, 'keyup', { key: 'Escape' }); expect(spy).toHaveBeenCalledWith(); }); + + it('should handle asynchronous popover references', () => { + const caller = directiveElements[4]; + const callerInstance = caller.injector.get(SkyPopoverDirective); + const eventListenerSpy = spyOn(callerInstance as any, 'addEventListeners').and.callThrough(); + + caller.nativeElement.click(); + fixture.detectChanges(); + + expect(callerInstance.skyPopover).toBeUndefined(); + expect(eventListenerSpy).not.toHaveBeenCalled(); + + eventListenerSpy.calls.reset(); + + fixture.componentInstance.attachAsyncPopover(); + fixture.detectChanges(); + + expect(callerInstance.skyPopover).toBeDefined(); + expect(eventListenerSpy).toHaveBeenCalled(); + }); + + it('should remove event listeners before adding them again', () => { + const caller = directiveElements[4]; + const callerInstance = caller.injector.get(SkyPopoverDirective); + const addEventSpy = spyOn(callerInstance as any, 'addEventListeners').and.callThrough(); + const removeEventSpy = spyOn(callerInstance as any, 'removeEventListeners').and.callThrough(); + + fixture.componentInstance.attachAsyncPopover(); + fixture.detectChanges(); + + fixture.componentInstance.attachAnotherAsyncPopover(); + fixture.detectChanges(); + + expect(callerInstance.skyPopover).toBeDefined(); + expect(addEventSpy.calls.count()).toEqual(2); + expect(removeEventSpy.calls.count()).toEqual(2); + }); + + it('should not add listeners to undefined popovers', () => { + const caller = directiveElements[4]; + const callerInstance = caller.injector.get(SkyPopoverDirective); + + fixture.componentInstance.attachAsyncPopover(); + fixture.detectChanges(); + + const addEventSpy = spyOn(callerInstance as any, 'addEventListeners').and.callThrough(); + const removeEventSpy = spyOn(callerInstance as any, 'removeEventListeners').and.callThrough(); + + fixture.componentInstance.asyncPopoverRef = undefined; + fixture.detectChanges(); + + expect(callerInstance.skyPopover).toBeUndefined(); + expect(addEventSpy).not.toHaveBeenCalled(); + expect(removeEventSpy).toHaveBeenCalled(); + }); }); diff --git a/src/modules/popover/popover.directive.ts b/src/modules/popover/popover.directive.ts index 7b676c005..fabed989b 100644 --- a/src/modules/popover/popover.directive.ts +++ b/src/modules/popover/popover.directive.ts @@ -1,10 +1,16 @@ import { Directive, ElementRef, - HostListener, - Input + Input, + OnChanges, + OnDestroy, + SimpleChanges } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/fromEvent'; +import { Subject } from 'rxjs/Subject'; + import { SkyWindowRefService } from '../window'; @@ -20,7 +26,7 @@ import { SkyPopoverComponent } from './popover.component'; @Directive({ selector: '[skyPopover]' }) -export class SkyPopoverDirective { +export class SkyPopoverDirective implements OnChanges, OnDestroy { @Input() public skyPopover: SkyPopoverComponent; @@ -33,28 +39,29 @@ export class SkyPopoverDirective { @Input() public skyPopoverTrigger: SkyPopoverTrigger = 'click'; + private idled = new Subject(); + constructor( private elementRef: ElementRef, private windowRef: SkyWindowRefService ) { } - @HostListener('keyup', ['$event']) - public onDocumentKeyUp(event: KeyboardEvent): void { - const key = event.key.toLowerCase(); - if (key === 'escape' && this.skyPopover.isOpen) { - event.stopPropagation(); - event.preventDefault(); - this.closePopover(); - this.elementRef.nativeElement.focus(); + public ngOnChanges(changes: SimpleChanges) { + /* istanbul ignore else */ + if (changes.skyPopover) { + this.removeEventListeners(); + if (changes.skyPopover.currentValue !== undefined) { + this.addEventListeners(); + } } } - @HostListener('click', ['$event']) - public togglePopover(event: MouseEvent) { - event.preventDefault(); - event.stopPropagation(); + public ngOnDestroy(): void { + this.removeEventListeners(); + } - if (this.skyPopover.isOpen) { + public togglePopover() { + if (this.isPopoverOpen()) { this.closePopover(); return; } @@ -62,36 +69,6 @@ export class SkyPopoverDirective { this.positionPopover(); } - @HostListener('mouseenter', ['$event']) - public onMouseEnter(event: MouseEvent) { - this.skyPopover.isMouseEnter = true; - if (this.skyPopoverTrigger === 'mouseenter') { - event.preventDefault(); - this.positionPopover(); - } - } - - @HostListener('mouseleave', ['$event']) - public onMouseLeave(event: MouseEvent) { - this.skyPopover.isMouseEnter = false; - - if (this.skyPopoverTrigger === 'mouseenter') { - event.preventDefault(); - - // Give the popover a chance to set its isMouseEnter flag before checking to see - // if it should be closed. - this.windowRef.getWindow().setTimeout(() => { - if (this.skyPopover.isOpen) { - if (this.skyPopover.isMouseEnter) { - this.skyPopover.markForCloseOnMouseLeave(); - } else { - this.closePopover(); - } - } - }); - } - } - private positionPopover() { this.skyPopover.positionNextTo( this.elementRef, @@ -103,4 +80,74 @@ export class SkyPopoverDirective { private closePopover() { this.skyPopover.close(); } + + private isPopoverOpen(): boolean { + return (this.skyPopover && this.skyPopover.isOpen); + } + + private addEventListeners() { + const element = this.elementRef.nativeElement; + + Observable + .fromEvent(element, 'keyup') + .takeUntil(this.idled) + .subscribe((event: KeyboardEvent) => { + const key = event.key.toLowerCase(); + if (key === 'escape' && this.isPopoverOpen()) { + event.stopPropagation(); + event.preventDefault(); + this.closePopover(); + this.elementRef.nativeElement.focus(); + } + }); + + Observable + .fromEvent(element, 'click') + .takeUntil(this.idled) + .subscribe((event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + this.togglePopover(); + }); + + Observable + .fromEvent(element, 'mouseenter') + .takeUntil(this.idled) + .subscribe((event: MouseEvent) => { + this.skyPopover.isMouseEnter = true; + if (this.skyPopoverTrigger === 'mouseenter') { + event.preventDefault(); + this.positionPopover(); + } + }); + + Observable + .fromEvent(element, 'mouseleave') + .takeUntil(this.idled) + .subscribe((event: MouseEvent) => { + this.skyPopover.isMouseEnter = false; + + if (this.skyPopoverTrigger === 'mouseenter') { + event.preventDefault(); + + // Give the popover a chance to set its isMouseEnter flag before checking to see + // if it should be closed. + this.windowRef.getWindow().setTimeout(() => { + if (this.isPopoverOpen()) { + if (this.skyPopover.isMouseEnter) { + this.skyPopover.markForCloseOnMouseLeave(); + } else { + this.closePopover(); + } + } + }); + } + }); + } + + private removeEventListeners() { + this.idled.next(true); + this.idled.unsubscribe(); + this.idled = new Subject(); + } }