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

Added skyPopoverTrigger attribute to allow mouseenter to show a popover #1172

Merged
merged 9 commits into from
Oct 11, 2017
Binary file not shown.
Binary file not shown.
7 changes: 7 additions & 0 deletions src/app/components/popover/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
isOptional="true">
Specifies the placement of the popover in relation to the trigger element (above, below, left, right).
</sky-demo-page-property>
<sky-demo-page-property
propertyName="skyPopoverTrigger"
defaultValue="click"
isOptional="true"
>
Specifies the user action that displays the popover (click, mouseenter).
</sky-demo-page-property>
</sky-demo-page-properties>

<sky-demo-page-properties sectionHeading="Popover component properties">
Expand Down
20 changes: 20 additions & 0 deletions src/app/components/popover/popover-demo.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,26 @@ <h3>
The content of a popover can be text, HTML, or Angular components.
</sky-popover>

<h3>
Popovers can also be opened when the mouse hovers over an element:
</h3>

<button
class="sky-btn"
[skyPopover]="popoverMouseEnter"
skyPopoverPlacement="right"
skyPopoverTrigger="mouseenter"
>
Hover
</button>

<sky-popover
popoverTitle="Did you know?"
#popoverMouseEnter
>
You can move your mouse off the button to close this popover.
</sky-popover>

<h3>
Events
</h3>
Expand Down
4 changes: 4 additions & 0 deletions src/modules/popover/fixtures/popover.component.fixture.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
Open
</button>

<button [skyPopover]="mypopover" skyPopoverPlacement="below" skyPopoverTrigger="mouseenter">
Open
</button>

<sky-popover #mypopover>
Some text.
</sky-popover>
1 change: 1 addition & 0 deletions src/modules/popover/index.ts
Original file line number Diff line number Diff line change
@@ -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';
131 changes: 61 additions & 70 deletions src/modules/popover/popover-adapter.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
} from './popover-adapter.service';

describe('SkyPopoverAdapterService', () => {
let elementRefDefinition: any;
let mockRenderer: any;

class MockWindowService {
Expand All @@ -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() {},
Expand All @@ -68,66 +76,62 @@ 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');
})
);

it('should only check for optimal placements a few times',
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();
Expand All @@ -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');
})
);
});
39 changes: 18 additions & 21 deletions src/modules/popover/popover-adapter.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -26,20 +23,13 @@ export interface SkyPopoverAdapterElements {

@Injectable()
export class SkyPopoverAdapterService {
public placementChanges: Observable<SkyPopoverPlacement>;
private placementSubject: Subject<SkyPopoverPlacement>;

constructor(
private renderer: Renderer2,
private windowRef: SkyWindowRefService
) {
this.placementSubject = new Subject<SkyPopoverPlacement>();
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);
Expand Down Expand Up @@ -79,8 +69,6 @@ export class SkyPopoverAdapterService {
coords = this.getCoordinates(elements, placement);
}

this.sendChanges(placement);

return coords;
}

Expand All @@ -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;
Expand All @@ -131,6 +120,7 @@ export class SkyPopoverAdapterService {
arrowTop = (popoverRect.height / 2);
break;
}
// tslint:enable:switch-default

left += window.pageXOffset;
top += window.pageYOffset;
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
}
}
1 change: 1 addition & 0 deletions src/modules/popover/popover-trigger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type SkyPopoverTrigger = 'click' | 'mouseenter';
2 changes: 1 addition & 1 deletion src/modules/popover/popover.component.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="sky-popover-container hidden"
[ngClass]="placementClassName"
[ngClass]="'sky-popover-placement-' + placement"
(@popoverState.start)="onAnimationStart($event)"
(@popoverState.done)="onAnimationDone($event)"
[@popoverState]="getAnimationState()"
Expand Down
Loading