Skip to content
This repository has been archived by the owner on Dec 8, 2022. It is now read-only.

Allow for async popover refs #1475

Merged
merged 2 commits into from
Feb 15, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/demos/popover/popover-demo.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,18 @@ <h3>
</sky-dropdown-menu>
</sky-dropdown>
</sky-popover>

<h3>
Popovers may also be asynchronous
</h3>

<button
type="button"
class="sky-btn sky-btn-default"
[skyPopover]="asyncPopoverRef">
This trigger has an asynchronous popover
</button>

<sky-popover #asyncPopover>
My asynchronous popover.
</sky-popover>
15 changes: 15 additions & 0 deletions src/demos/popover/popover-demo.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
15 changes: 15 additions & 0 deletions src/modules/popover/fixtures/popover.component.fixture.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,19 @@
<sky-popover #mypopover4>
Some text.
</sky-popover>

<button
type="button"
class="sky-btn sky-btn-default"
[skyPopover]="asyncPopoverRef">
This trigger has an asynchronous popover
</button>

<sky-popover #asyncPopover>
My asynchronous popover.
</sky-popover>

<sky-popover #anotherAsyncPopover>
My asynchronous popover.
</sky-popover>
</div>
20 changes: 19 additions & 1 deletion src/modules/popover/fixtures/popover.component.fixture.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
55 changes: 55 additions & 0 deletions src/modules/popover/popover.directive.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
139 changes: 93 additions & 46 deletions src/modules/popover/popover.directive.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;

Expand All @@ -33,65 +39,36 @@ export class SkyPopoverDirective {
@Input()
public skyPopoverTrigger: SkyPopoverTrigger = 'click';

private idled = new Subject<boolean>();

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;
}

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,
Expand All @@ -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<boolean>();
}
}