diff --git a/docs/articles/concept-ui-kit.md b/docs/articles/concept-ui-kit.md
index 82d4095eed..b3ba61135f 100644
--- a/docs/articles/concept-ui-kit.md
+++ b/docs/articles/concept-ui-kit.md
@@ -11,6 +11,7 @@ Here's a list of Nebular components:
- [Actions](#/docs/components/actions)
- [User(Avatar)](#/docs/components/user-avatar)
- [Checkbox](#/docs/components/checkbox)
+- [Popover](#/docs/components/popover)
diff --git a/docs/assets/images/components/popover.gif b/docs/assets/images/components/popover.gif
new file mode 100644
index 0000000000..4b4e1ba347
Binary files /dev/null and b/docs/assets/images/components/popover.gif differ
diff --git a/docs/structure.ts b/docs/structure.ts
index 0839ca0506..31df7e44a2 100644
--- a/docs/structure.ts
+++ b/docs/structure.ts
@@ -394,6 +394,22 @@ export const STRUCTURE = [
},
],
},
+ {
+ type: 'page',
+ name: 'Popover',
+ children: [
+ {
+ type: 'block',
+ block: 'component',
+ blockData: 'NbPopoverDirective',
+ },
+ {
+ type: 'block',
+ block: 'component',
+ blockData: 'NbPopoverComponent',
+ },
+ ],
+ },
],
},
{
diff --git a/e2e/popover.e2e-spec.ts b/e2e/popover.e2e-spec.ts
new file mode 100644
index 0000000000..070f96d093
--- /dev/null
+++ b/e2e/popover.e2e-spec.ts
@@ -0,0 +1,154 @@
+import { browser, by, element } from 'protractor';
+
+const contentTemplate = by.css('nb-card:nth-child(1) button:nth-child(1)');
+const contentComponent = by.css('nb-card:nth-child(1) button:nth-child(2)');
+const contentString = by.css('nb-card:nth-child(1) button:nth-child(3)');
+const placementRight = by.css('nb-card:nth-child(2) button:nth-child(1)');
+const placementBottom = by.css('nb-card:nth-child(2) button:nth-child(2)');
+const placementTop = by.css('nb-card:nth-child(2) button:nth-child(3)');
+const placementLeft = by.css('nb-card:nth-child(2) button:nth-child(4)');
+const modeClick = by.css('nb-card:nth-child(4) button:nth-child(1)');
+const modeHover = by.css('nb-card:nth-child(4) button:nth-child(2)');
+const modeHint = by.css('nb-card:nth-child(4) button:nth-child(3)');
+const popover = by.css('nb-layout > nb-popover');
+
+describe('nb-popover', () => {
+
+ beforeEach((done) => {
+ browser.get('#/popover').then(done);
+ });
+
+ it('render template ref', () => {
+ element(contentTemplate).click();
+ const containerContent = element(popover).element(by.css('nb-card'));
+ expect(containerContent.isPresent()).toBeTruthy();
+ });
+
+ it('render component ref', () => {
+ element(contentComponent).click();
+ const containerContent = element(popover).element(by.css('nb-dynamic-to-add'));
+ expect(containerContent.isPresent()).toBeTruthy();
+ });
+
+ it('render primitive', () => {
+ element(contentString).click();
+ const containerContent = element(popover).element(by.css('div'));
+ expect(containerContent.isPresent()).toBeTruthy();
+ expect(containerContent.getAttribute('class')).toEqual('primitive-popover');
+ expect(containerContent.getText()).toEqual('Hi, I\'m popover!');
+ });
+
+ it('render container with arrow', () => {
+ element(contentTemplate).click();
+ const arrow = element(popover).element(by.css('span'));
+ expect(arrow.getAttribute('class')).toEqual('arrow');
+ });
+
+ it('render container in the right', () => {
+ element(placementRight).click();
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+ expect(container.getAttribute('class')).toEqual('right');
+ });
+
+ it('render container in the bottom', () => {
+ element(placementBottom).click();
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+ expect(container.getAttribute('class')).toEqual('bottom');
+ });
+
+ it('render container in the top', () => {
+ element(placementTop).click();
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+ expect(container.getAttribute('class')).toEqual('top');
+ });
+
+ it('render container in the left', () => {
+ element(placementLeft).click();
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+ expect(container.getAttribute('class')).toEqual('left');
+ });
+
+ it('open popover by host click', () => {
+ element(modeClick).click();
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+ });
+
+ it('close popover by host click', () => {
+ element(modeClick).click();
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+ element(modeClick).click();
+ expect(container.isPresent()).toBeFalsy();
+ });
+
+ it('doesn\'t close popover by container click', () => {
+ element(modeClick).click();
+ const container = element(popover);
+ container.click();
+ expect(container.isPresent()).toBeTruthy();
+ });
+
+ it('close popover by click outside the host and container', () => {
+ element(modeClick).click();
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+ browser.actions()
+ .mouseMove(container, { x: -10, y: -10 })
+ .click()
+ .perform();
+ expect(container.isPresent()).toBeFalsy();
+ });
+
+ it('open popover by hover on host', () => {
+ browser.actions()
+ .mouseMove(element(modeHover))
+ .perform();
+
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+ });
+
+ it('close popover by hover out host', () => {
+ browser.actions()
+ .mouseMove(element(modeHover))
+ .perform();
+
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+
+ browser.actions()
+ .mouseMove(element(modeClick))
+ .perform();
+
+ expect(container.isPresent()).toBeFalsy();
+ });
+
+ it('doesn\'t close popover by hover on container', () => {
+ browser.actions()
+ .mouseMove(element(modeHover))
+ .perform();
+
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+
+ browser.actions()
+ .mouseMove(container)
+ .perform();
+
+ expect(container.isPresent()).toBeTruthy();
+ });
+
+ it('open popover by hover on host with hint', () => {
+ browser.actions()
+ .mouseMove(element(modeHint))
+ .perform();
+
+ const container = element(popover);
+ expect(container.isPresent()).toBeTruthy();
+ });
+});
diff --git a/gulpfile.js b/gulpfile.js
index 27edc95c7d..7832656e69 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -72,6 +72,7 @@ const ROLLUP_GLOBALS = {
'rxjs/operators/pairwise': 'Rx.operators',
'rxjs/operators/distinctUntilChanged': 'Rx.operators',
'rxjs/operators/takeWhile': 'Rx.operators',
+ 'rxjs/operators/repeat': 'Rx.operators',
// 3rd party dependencies
diff --git a/package-lock.json b/package-lock.json
index 22ae52fb05..d75a67dcd2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4140,9 +4140,9 @@
}
},
"doc-prsr": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/doc-prsr/-/doc-prsr-2.0.1.tgz",
- "integrity": "sha512-Wo5RafWRPW72NTkr18viwEFkw4A0I1B4IE/Hhl5UGJ/OVe8s4c6hE7s2CKn0f/5InJBLTVKm7tiZ4utzwCFdZQ==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/doc-prsr/-/doc-prsr-2.0.2.tgz",
+ "integrity": "sha512-TyeNk7GMLd6oACKwI6azIwKFVZmv0DhGdTuI0OlBB0MzczbZpPVGduQ+cDTUyvnY6mBhpvK0K5RAPPaCYt0ZAA==",
"dev": true,
"requires": {
"commander": "2.11.0",
diff --git a/package.json b/package.json
index eed1b6c637..2359a0b76c 100644
--- a/package.json
+++ b/package.json
@@ -109,7 +109,7 @@
"browserstack-local": "1.3.0",
"codelyzer": "4.0.2",
"conventional-changelog-cli": "1.3.4",
- "doc-prsr": "2.0.1",
+ "doc-prsr": "2.0.2",
"firebase-tools": "^3.17.4",
"gulp": "3.9.1",
"gulp-autoprefixer": "3.1.1",
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 83235dfe1e..4207988fef 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -22,6 +22,7 @@ import {
NbTabsetModule,
NbThemeModule,
NbUserModule,
+ NbPopoverModule,
} from '@nebular/theme';
import {
@@ -67,13 +68,15 @@ import { NbUserTestComponent } from './user-test/user-test.component';
import { NbDynamicToAddComponent, NbThemeDynamicTestComponent } from './layout-test/theme-dynamic-test.component';
import { NbActionsTestComponent } from './actions-test/actions-test.component';
import { NbBootstrapTestComponent } from './bootstrap-test/bootstrap-test.component';
+import { NbPopoverTestComponent } from './popover-test/popover-test.component';
+
+import { routes } from './app.routes';
+
import { NbCheckboxTestComponent } from './checkbox-test/checkbox-test.component';
import { NbSearchTestComponent } from './search-test/search-test.component';
import { NbSearchTestCustomizedComponent } from './search-test/search-test-customized.component';
import { NbFormsTestComponent } from './forms-test/forms-test.component';
-import { routes } from './app.routes';
-
import { NbCardTestComponent } from './card-test/card-test.component';
import { NbAclTestComponent } from './acl-test/acl-test.component';
import { AuthGuard } from './auth-guard.service';
@@ -113,6 +116,7 @@ const NB_TEST_COMPONENTS = [
NbThemeBreakpointTestComponent,
NbActionsTestComponent,
NbFormsTestComponent,
+ NbPopoverTestComponent,
NbCheckboxTestComponent,
NbAclTestComponent,
];
@@ -133,6 +137,7 @@ const NB_TEST_COMPONENTS = [
NbUserModule,
NbSearchModule,
NbActionsModule,
+ NbPopoverModule,
NbCheckboxModule,
NbAuthModule.forRoot({
forms: {
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index ef913be508..ff886b9674 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -41,6 +41,7 @@ import { NbSidebarTestComponent } from './sidebar-test/sidebar-test.component';
import { NbTabsetTestComponent } from './tabset-test/tabset-test.component';
import { NbUserTestComponent } from './user-test/user-test.component';
import { NbCardTestComponent } from './card-test/card-test.component';
+import { NbPopoverTestComponent } from './popover-test/popover-test.component';
import {
NbAuthComponent,
NbLoginComponent,
@@ -249,6 +250,10 @@ export const routes: Routes = [
path: 'forms',
component: NbFormsTestComponent,
},
+ {
+ path: 'popover',
+ component: NbPopoverTestComponent,
+ },
{
path: 'acl',
component: NbAclTestComponent,
diff --git a/src/app/popover-test/popover-test.component.ts b/src/app/popover-test/popover-test.component.ts
new file mode 100644
index 0000000000..96bb99622d
--- /dev/null
+++ b/src/app/popover-test/popover-test.component.ts
@@ -0,0 +1,143 @@
+/**
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import { Component } from '@angular/core';
+import { NbDynamicToAddComponent } from '../layout-test/theme-dynamic-test.component';
+
+@Component({
+ selector: 'nb-popover-test',
+ template: `
+
+
+
+
+ Content Type
+
+
+
+
+
+
+
+ Help text
+
+
+
+
+
+
+
+
+
+
+
+ Placement
+
+
+
+
+
+
+
+
+
+ Multiple Hints
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Trigger mode
+
+
+
+
+
+
+
+
+ Popover
+
+
+
+
+
+
+ `,
+})
+export class NbPopoverTestComponent {
+
+ customPopoverComponent = NbDynamicToAddComponent;
+}
diff --git a/src/framework/theme/components/popover/_popover.component.theme.scss b/src/framework/theme/components/popover/_popover.component.theme.scss
new file mode 100644
index 0000000000..1476d42186
--- /dev/null
+++ b/src/framework/theme/components/popover/_popover.component.theme.scss
@@ -0,0 +1,26 @@
+/**
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+@mixin nb-popover-theme {
+ nb-popover {
+ $arrow-size: 11px;
+ border: 2px solid nb-theme(popover-border);
+ background: nb-theme(popover-bg);
+ box-shadow: nb-theme(popover-shadow);
+
+ .primitive-popover {
+ color: nb-theme(popover-fg);
+ }
+
+ .arrow {
+ border-bottom: $arrow-size solid nb-theme(popover-border);
+
+ &::after {
+ border-bottom: $arrow-size solid nb-theme(popover-bg);
+ }
+ }
+ }
+}
diff --git a/src/framework/theme/components/popover/helpers/adjustment.helper.ts b/src/framework/theme/components/popover/helpers/adjustment.helper.ts
new file mode 100644
index 0000000000..a582e2446e
--- /dev/null
+++ b/src/framework/theme/components/popover/helpers/adjustment.helper.ts
@@ -0,0 +1,104 @@
+import { NbPositioningHelper } from './positioning.helper';
+import { NbPopoverAdjustment, NbPopoverPlacement, NbPopoverPosition } from './model';
+
+/**
+ * Describes the bypass order of the {@link NbPopoverPlacement} in the {@link NbPopoverAdjustment}.
+ * */
+const NB_ORDERED_PLACEMENTS = {
+ [NbPopoverAdjustment.CLOCKWISE]: [
+ NbPopoverPlacement.TOP,
+ NbPopoverPlacement.RIGHT,
+ NbPopoverPlacement.BOTTOM,
+ NbPopoverPlacement.LEFT,
+ ],
+
+ [NbPopoverAdjustment.COUNTERCLOCKWISE]: [
+ NbPopoverPlacement.TOP,
+ NbPopoverPlacement.LEFT,
+ NbPopoverPlacement.BOTTOM,
+ NbPopoverPlacement.RIGHT,
+ ],
+};
+
+export class NbAdjustmentHelper {
+
+ /**
+ * Calculated {@link NbPopoverPosition} based on placed element, host element,
+ * placed element placement and adjustment strategy.
+ *
+ * @param placed {ClientRect} placed element relatively host.
+ * @param host {ClientRect} host element.
+ * @param placement {NbPopoverPlacement} placed element placement relatively host.
+ * @param adjustment {NbPopoverAdjustment} adjustment strategy.
+ *
+ * @return {NbPopoverPosition} calculated position.
+ * */
+ static adjust(placed: ClientRect,
+ host: ClientRect,
+ placement: NbPopoverPlacement,
+ adjustment: NbPopoverAdjustment): NbPopoverPosition {
+ const placements = NB_ORDERED_PLACEMENTS[adjustment].slice();
+ const ordered = NbAdjustmentHelper.orderPlacements(placement, placements);
+ const possible = ordered.map(pl => ({
+ position: NbPositioningHelper.calcPosition(placed, host, pl),
+ placement: pl,
+ }));
+
+ return NbAdjustmentHelper.chooseBest(placed, possible);
+ }
+
+ /**
+ * Searches first adjustment which doesn't go beyond the viewport.
+ *
+ * @param placed {ClientRect} placed element relatively host.
+ * @param possible {NbPopoverPosition[]} possible positions list ordered according to adjustment strategy.
+ *
+ * @return {NbPopoverPosition} calculated position.
+ * */
+ private static chooseBest(placed: ClientRect, possible: NbPopoverPosition[]): NbPopoverPosition {
+ return possible.find(adjust => NbAdjustmentHelper.inViewPort(placed, adjust)) || possible.shift();
+ }
+
+ /**
+ * Finds out is adjustment doesn't go beyond of the view port.
+ *
+ * @param placed {ClientRect} placed element relatively host.
+ * @param position {NbPopoverPosition} position of the placed element.
+ *
+ * @return {boolean} true if placed element completely viewport.
+ * */
+ private static inViewPort(placed: ClientRect, position: NbPopoverPosition): boolean {
+ return position.position.top - window.pageYOffset > 0
+ && position.position.left - window.pageXOffset > 0
+ && position.position.top + placed.height < window.innerHeight + window.pageYOffset
+ && position.position.left + placed.width < window.innerWidth + window.pageXOffset;
+ }
+
+ /**
+ * Reorder placements list to make placement start point and fit {@link NbPopoverAdjustment}
+ *
+ * @param placement {NbPopoverPlacement} active placement
+ * @param placements {NbPopoverPlacement[]} placements list according to the active adjustment strategy.
+ *
+ * @return {NbPopoverPlacement[]} correctly ordered placements list.
+ *
+ * @example order placements for {@link NbPopoverPlacement#RIGHT} and {@link NbPopoverAdjustment#CLOCKWISE}
+ * ```
+ * const placements = NB_ORDERED_PLACEMENTS[NbPopoverAdjustment.CLOCKWISE];
+ * const ordered = orderPlacement(NbPopoverPlacement.RIGHT, placements);
+ *
+ * expect(ordered).toEqual([
+ * NbPopoverPlacement.RIGHT,
+ * NbPopoverPlacement.BOTTOM,
+ * NbPopoverPlacement.LEFT,
+ * NbPopoverPlacement.TOP,
+ * ]);
+ * ```
+ * */
+ private static orderPlacements(placement: NbPopoverPlacement,
+ placements: NbPopoverPlacement[]): NbPopoverPlacement[] {
+ const index = placements.indexOf(placement);
+ const start = placements.splice(index, placements.length);
+ return start.concat(...placements);
+ }
+}
diff --git a/src/framework/theme/components/popover/helpers/adjustment.spec.ts b/src/framework/theme/components/popover/helpers/adjustment.spec.ts
new file mode 100644
index 0000000000..75f2ac60d8
--- /dev/null
+++ b/src/framework/theme/components/popover/helpers/adjustment.spec.ts
@@ -0,0 +1,220 @@
+/**
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import { NbAdjustmentHelper } from './adjustment.helper';
+import { NbPopoverAdjustment, NbPopoverPlacement } from './model';
+
+describe('adjustment-helper', () => {
+ const placedRect: ClientRect = {
+ top: 50,
+ bottom: 100,
+ left: 50,
+ right: 100,
+ height: 50,
+ width: 50,
+ };
+
+ const hostRect = {
+ topLeft: {
+ top: 10,
+ bottom: 110,
+ left: 10,
+ right: 110,
+ height: 100,
+ width: 100,
+ },
+ topRight: {
+ top: 10,
+ bottom: 110,
+ left: 1000,
+ right: 1100,
+ height: 100,
+ width: 100,
+ },
+ bottomLeft: {
+ top: 1000,
+ bottom: 1100,
+ left: 10,
+ right: 110,
+ height: 100,
+ width: 100,
+ },
+ bottomRight: {
+ top: 1000,
+ bottom: 1100,
+ left: 1000,
+ right: 1100,
+ height: 100,
+ width: 100,
+ },
+ };
+
+ describe('clockwise strategy', () => {
+ const strategy = NbPopoverAdjustment.CLOCKWISE;
+ const placement = NbPopoverPlacement.TOP;
+
+ it('adjust top to right when host in top left corner', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.topLeft, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.RIGHT);
+ expect(adjustment.position.top).toEqual(1035);
+ expect(adjustment.position.left).toEqual(1120);
+ });
+
+ it('adjust top to bottom when host in top right corner', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.topRight, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.BOTTOM);
+ expect(adjustment.position.top).toEqual(1120);
+ expect(adjustment.position.left).toEqual(2025);
+ });
+
+ it('doesn\'t adjust top when in bottom right corner', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.bottomRight, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.TOP);
+ expect(adjustment.position.top).toEqual(1940);
+ expect(adjustment.position.left).toEqual(2025);
+ });
+
+ it('doesn\'t adjust top when in bottom left corner', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.bottomLeft, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.TOP);
+ expect(adjustment.position.top).toEqual(1940);
+ expect(adjustment.position.left).toEqual(1035);
+ });
+
+ it('adjust top to left when host in the right part of the narrow rectangular view port', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(120);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.topRight, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.LEFT);
+ expect(adjustment.position.top).toEqual(1035);
+ expect(adjustment.position.left).toEqual(1940);
+ });
+
+ it('doesn\'t change position when there are no suitable positions at all', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(120);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(120);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.topLeft, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.TOP);
+ expect(adjustment.position.top).toEqual(950);
+ expect(adjustment.position.left).toEqual(1035);
+ });
+ });
+
+ describe('counterclockwise strategy', () => {
+ const strategy = NbPopoverAdjustment.COUNTERCLOCKWISE;
+ const placement = NbPopoverPlacement.TOP;
+
+ it('adjust top to bottom when host in top left corner', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.topLeft, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.BOTTOM);
+ expect(adjustment.position.top).toEqual(1120);
+ expect(adjustment.position.left).toEqual(1035);
+ });
+
+ it('adjust top to left when host in top right corner', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.topRight, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.LEFT);
+ expect(adjustment.position.top).toEqual(1035);
+ expect(adjustment.position.left).toEqual(1940);
+ });
+
+ it('doesn\'t adjust top when in bottom right corner', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.bottomRight, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.TOP);
+ expect(adjustment.position.top).toEqual(1940);
+ expect(adjustment.position.left).toEqual(2025);
+ });
+
+ it('doesn\'t adjust top when in bottom left corner', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.bottomLeft, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.TOP);
+ expect(adjustment.position.top).toEqual(1940);
+ expect(adjustment.position.left).toEqual(1035);
+ });
+
+ it('adjust top to left when host in the right part of the narrow rectangular view port', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(120);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(1110);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.topRight, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.LEFT);
+ expect(adjustment.position.top).toEqual(1035);
+ expect(adjustment.position.left).toEqual(1940);
+ });
+
+ it('doesn\'t change position when there are no suitable positions at all', () => {
+ spyOnProperty(window, 'innerHeight', 'get').and.returnValue(120);
+ spyOnProperty(window, 'innerWidth', 'get').and.returnValue(120);
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const adjustment = NbAdjustmentHelper.adjust(placedRect, hostRect.topLeft, placement, strategy);
+
+ expect(adjustment.placement).toEqual(NbPopoverPlacement.TOP);
+ expect(adjustment.position.top).toEqual(950);
+ expect(adjustment.position.left).toEqual(1035);
+ });
+ });
+});
diff --git a/src/framework/theme/components/popover/helpers/model.ts b/src/framework/theme/components/popover/helpers/model.ts
new file mode 100644
index 0000000000..5c34569de6
--- /dev/null
+++ b/src/framework/theme/components/popover/helpers/model.ts
@@ -0,0 +1,50 @@
+import { Observable } from 'rxjs/Observable';
+
+/**
+ * Describes placement of the UI element on the screen.
+ * */
+export class NbPopoverPosition {
+ placement: NbPopoverPlacement;
+ position: {
+ top: number;
+ left: number;
+ };
+}
+
+/**
+ * Adjustment strategies.
+ * */
+export enum NbPopoverAdjustment {
+ CLOCKWISE = 'clockwise',
+ COUNTERCLOCKWISE = 'counterclockwise',
+}
+
+/**
+ * Arrangement of one element relative to another.
+ * */
+export enum NbPopoverPlacement {
+ TOP = 'top',
+ BOTTOM = 'bottom',
+ LEFT = 'left',
+ RIGHT = 'right',
+}
+
+/**
+ * NbPopoverMode describes when to trigger show and hide methods of the popover.
+ * */
+export enum NbPopoverMode {
+ CLICK = 'click',
+ HOVER = 'hover',
+ HINT = 'hint',
+}
+
+/**
+ * Popover uses different triggers for different {@link NbPopoverMode}.
+ * see {@link NbTriggerHelper}
+ * */
+export class NbPopoverTrigger {
+ toggle: Observable
;
+ open: Observable;
+ close: Observable;
+}
+
diff --git a/src/framework/theme/components/popover/helpers/positioning.helper.ts b/src/framework/theme/components/popover/helpers/positioning.helper.ts
new file mode 100644
index 0000000000..5fd7be0f93
--- /dev/null
+++ b/src/framework/theme/components/popover/helpers/positioning.helper.ts
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+import { NbPopoverPlacement } from './model';
+
+export class NbPositioningHelper {
+
+ /**
+ * Describes height of the popover arrow.
+ * */
+ private static ARROW_SIZE: number = 10;
+
+ /**
+ * Contains position calculators for all {@link NbPopoverPlacement}
+ * */
+ private static positionCalculator = {
+ [NbPopoverPlacement.TOP](positioned: ClientRect, host: ClientRect): { top: number, left: number } {
+ return {
+ top: host.top - positioned.height - NbPositioningHelper.ARROW_SIZE,
+ left: host.left + host.width / 2 - positioned.width / 2,
+ }
+ },
+
+ [NbPopoverPlacement.BOTTOM](positioned: ClientRect, host: ClientRect): { top: number, left: number } {
+ return {
+ top: host.top + host.height + NbPositioningHelper.ARROW_SIZE,
+ left: host.left + host.width / 2 - positioned.width / 2,
+ }
+ },
+
+ [NbPopoverPlacement.LEFT](positioned: ClientRect, host: ClientRect): { top: number, left: number } {
+ return {
+ top: host.top + host.height / 2 - positioned.height / 2,
+ left: host.left - positioned.width - NbPositioningHelper.ARROW_SIZE,
+ }
+ },
+
+ [NbPopoverPlacement.RIGHT](positioned: ClientRect, host: ClientRect): { top: number, left: number } {
+ return {
+ top: host.top + host.height / 2 - positioned.height / 2,
+ left: host.left + host.width + NbPositioningHelper.ARROW_SIZE,
+ }
+ },
+ };
+
+ /**
+ * Calculates position of the element relatively to the host element based on the placement.
+ * */
+ static calcPosition(positioned: ClientRect,
+ host: ClientRect,
+ placement: NbPopoverPlacement): { top: number, left: number } {
+ const positionCalculator: Function = NbPositioningHelper.positionCalculator[placement];
+ const position = positionCalculator.call(NbPositioningHelper.positionCalculator, positioned, host);
+
+ position.top += window.pageYOffset;
+ position.left += window.pageXOffset;
+
+ return position;
+ }
+}
diff --git a/src/framework/theme/components/popover/helpers/positioning.spec.ts b/src/framework/theme/components/popover/helpers/positioning.spec.ts
new file mode 100644
index 0000000000..0e5df52636
--- /dev/null
+++ b/src/framework/theme/components/popover/helpers/positioning.spec.ts
@@ -0,0 +1,92 @@
+/**
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import { NbPositioningHelper } from './positioning.helper';
+import { NbPopoverPlacement } from './model';
+
+describe('positioning-helper', () => {
+ const placedRect: ClientRect = {
+ top: 50,
+ bottom: 100,
+ left: 50,
+ right: 100,
+ height: 50,
+ width: 50,
+ };
+
+ const hostRect: ClientRect = {
+ top: 100,
+ bottom: 200,
+ left: 100,
+ right: 200,
+ height: 100,
+ width: 100,
+ };
+
+ it('correctly locates top placement', () => {
+ const position = NbPositioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.TOP);
+ expect(position.top).toEqual(40);
+ expect(position.left).toEqual(125);
+ });
+
+ it('correctly locates bottom placement', () => {
+ const position = NbPositioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.BOTTOM);
+ expect(position.top).toEqual(210);
+ expect(position.left).toEqual(125);
+ });
+
+ it('correctly locates left placement', () => {
+ const position = NbPositioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.LEFT);
+ expect(position.top).toEqual(125);
+ expect(position.left).toEqual(40);
+ });
+
+ it('correctly locates right placement', () => {
+ const position = NbPositioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.RIGHT);
+ expect(position.top).toEqual(125);
+ expect(position.left).toEqual(210);
+ });
+
+ it('correctly locates top placement when view port has offset', () => {
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const position = NbPositioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.TOP);
+
+ expect(position.top).toEqual(1040);
+ expect(position.left).toEqual(1125);
+ });
+
+ it('correctly locates bottom placement when view port has offset', () => {
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const position = NbPositioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.BOTTOM);
+
+ expect(position.top).toEqual(1210);
+ expect(position.left).toEqual(1125);
+ });
+
+ it('correctly locates left placement when view port has offset', () => {
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const position = NbPositioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.LEFT);
+
+ expect(position.top).toEqual(1125);
+ expect(position.left).toEqual(1040);
+ });
+
+ it('correctly locates right placement when view port has offset', () => {
+ spyOnProperty(window, 'pageXOffset', 'get').and.returnValue(1000);
+ spyOnProperty(window, 'pageYOffset', 'get').and.returnValue(1000);
+
+ const position = NbPositioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.RIGHT);
+
+ expect(position.top).toEqual(1125);
+ expect(position.left).toEqual(1210);
+ });
+});
diff --git a/src/framework/theme/components/popover/helpers/trigger.helper.ts b/src/framework/theme/components/popover/helpers/trigger.helper.ts
new file mode 100644
index 0000000000..cb934ccd20
--- /dev/null
+++ b/src/framework/theme/components/popover/helpers/trigger.helper.ts
@@ -0,0 +1,115 @@
+import { fromEvent as observableFromEvent } from 'rxjs/observable/fromEvent';
+import { empty as observableEmpty } from 'rxjs/observable/empty';
+import { NbPopoverMode, NbPopoverTrigger } from './model';
+import { filter } from 'rxjs/operators/filter';
+import { delay } from 'rxjs/operators/delay';
+import { takeWhile } from 'rxjs/operators/takeWhile';
+import { debounceTime } from 'rxjs/operators/debounceTime';
+import { switchMap } from 'rxjs/operators/switchMap';
+import { repeat } from 'rxjs/operators/repeat';
+import { takeUntil } from 'rxjs/operators/takeUntil';
+
+
+/**
+ * Describes popover triggers strategies based on popover {@link NbPopoverMode} mode.
+ * */
+const NB_TRIGGERS = {
+
+ /**
+ * Creates toggle and close events streams based on popover {@link NbPopoverMode#CLICK} mode.
+ * Fires toggle event when click was performed on the host element.
+ * Fires close event when click was performed on the document but
+ * not on the host or container or popover container isn't rendered yet.
+ *
+ * @param host {HTMLElement} popover host element.
+ * @param getContainer {Function} popover container getter.
+ *
+ * @return {NbPopoverTrigger} open and close events streams.
+ * */
+ [NbPopoverMode.CLICK](host: HTMLElement, getContainer: Function): NbPopoverTrigger {
+ return {
+ open: observableEmpty(),
+ close: observableFromEvent(document, 'click')
+ .pipe(
+ filter(event => !host.contains(event.target as Node)
+ && getContainer()
+ && !getContainer().location.nativeElement.contains(event.target)),
+ ),
+ toggle: observableFromEvent(host, 'click'),
+ };
+ },
+
+ /**
+ * Creates open and close events streams based on popover {@link NbPopoverMode#HOVER} mode.
+ * Fires open event when mouse hovers over the host element and stay over at least 100 milliseconds.
+ * Fires close event when mouse leaves the host element and stops out of the host and popover container.
+ *
+ * @param host {HTMLElement} popover host element.
+ * @param getContainer {Function} popover container getter.
+ *
+ * @return {NbPopoverTrigger} open and close events streams.
+ * */
+ [NbPopoverMode.HOVER](host: HTMLElement, getContainer: Function): NbPopoverTrigger {
+ return {
+ open: observableFromEvent(host, 'mouseenter')
+ .pipe(
+ delay(100),
+ takeUntil(observableFromEvent(host, 'mouseleave')),
+ repeat(),
+ ),
+ close: observableFromEvent(host, 'mouseleave')
+ .pipe(
+ switchMap(() => observableFromEvent(document, 'mousemove')
+ .pipe(
+ debounceTime(100),
+ takeWhile(() => !!getContainer()),
+ filter(event => !host.contains(event.target as Node)
+ && !getContainer().location.nativeElement.contains(event.target),
+ ),
+ ),
+ ),
+ ),
+ toggle: observableEmpty(),
+ }
+ },
+
+ /**
+ * Creates open and close events streams based on popover {@link NbPopoverMode#HOVER} mode.
+ * Fires open event when mouse hovers over the host element and stay over at least 100 milliseconds.
+ * Fires close event when mouse leaves the host element.
+ *
+ * @param host {HTMLElement} popover host element.
+ *
+ * @return {NbPopoverTrigger} open and close events streams.
+ * */
+ [NbPopoverMode.HINT](host: HTMLElement): NbPopoverTrigger {
+ return {
+ open: observableFromEvent(host, 'mouseenter')
+ .pipe(
+ delay(100),
+ takeUntil(observableFromEvent(host, 'mouseleave')),
+ repeat(),
+ ),
+ close: observableFromEvent(host, 'mouseleave'),
+ toggle: observableEmpty(),
+ }
+ },
+};
+
+export class NbTriggerHelper {
+
+ /**
+ * Creates open and close events streams based on popover {@link NbPopoverMode} mode.
+ *
+ * @param host {HTMLElement} popover host element.
+ * @param getContainer {Function} popover container getter.
+ * Getter required because listen can be called when container isn't initialized.
+ * @param mode {NbPopoverMode} describes container triggering strategy.
+ *
+ * @return {NbPopoverTrigger} open and close events streams.
+ * */
+ static createTrigger(host: HTMLElement, getContainer: Function, mode: NbPopoverMode): NbPopoverTrigger {
+ const createTrigger = NB_TRIGGERS[mode];
+ return createTrigger.call(NB_TRIGGERS, host, getContainer);
+ }
+}
diff --git a/src/framework/theme/components/popover/popover.component.scss b/src/framework/theme/components/popover/popover.component.scss
new file mode 100644
index 0000000000..38ba6e51d9
--- /dev/null
+++ b/src/framework/theme/components/popover/popover.component.scss
@@ -0,0 +1,65 @@
+/**
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+:host {
+ $arrow-size: 11px;
+ $arrow-offset: -($arrow-size * 2);
+
+ position: absolute;
+ z-index: 10000;
+ border-radius: 5px;
+ top: 200px;
+
+ .primitive-popover {
+ padding: 0.75rem 1rem;
+ }
+
+ .arrow {
+ position: absolute;
+
+ width: 0;
+ height: 0;
+ }
+
+ .arrow {
+ border-left: $arrow-size solid transparent;
+ border-right: $arrow-size solid transparent;
+
+ &::after {
+ position: absolute;
+ content: ' ';
+ width: 0;
+ height: 0;
+ top: 3px;
+ left: calc(50% - #{$arrow-size});
+ border-left: $arrow-size solid transparent;
+ border-right: $arrow-size solid transparent;
+ }
+ }
+
+ &.bottom .arrow {
+ top: -#{$arrow-size};
+ left: calc(50% - #{$arrow-size});
+ }
+
+ &.left .arrow {
+ right: -#{$arrow-size + $arrow-size / 2};
+ top: calc(50% - #{$arrow-size / 2});
+ transform: rotate(90deg);
+ }
+
+ &.top .arrow {
+ bottom: -#{$arrow-size};
+ left: calc(50% - #{$arrow-size});
+ transform: rotate(180deg);
+ }
+
+ &.right .arrow {
+ left: -#{$arrow-size + $arrow-size / 2};
+ top: calc(50% - #{$arrow-size / 2});
+ transform: rotate(270deg);
+ }
+}
diff --git a/src/framework/theme/components/popover/popover.component.ts b/src/framework/theme/components/popover/popover.component.ts
new file mode 100644
index 0000000000..a79e4128c4
--- /dev/null
+++ b/src/framework/theme/components/popover/popover.component.ts
@@ -0,0 +1,88 @@
+/**
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import { Component, HostBinding, Input, TemplateRef, Type } from '@angular/core';
+import { NbPopoverPlacement } from './helpers/model';
+
+/**
+ * Popover can be one of the following types:
+ * template, component or plain js string.
+ * So NbPopoverContent provides types alias for this purposes.
+ * */
+export type NbPopoverContent = string | TemplateRef | Type;
+
+/**
+ * Popover container.
+ * Renders provided content inside.
+ *
+ * @styles
+ *
+ * popover-fg
+ * popover-bg
+ * popover-border
+ * popover-shadow
+ * */
+@Component({
+ selector: 'nb-popover',
+ styleUrls: ['./popover.component.scss'],
+ template: `
+
+
+
+
+
+ {{content}}
+
+ `,
+})
+export class NbPopoverComponent {
+
+ /**
+ * Content which will be rendered.
+ * */
+ @Input()
+ content: NbPopoverContent;
+
+ /**
+ * Popover placement relatively host element.
+ * */
+ @Input()
+ @HostBinding('class')
+ placement: NbPopoverPlacement = NbPopoverPlacement.TOP;
+
+ @Input()
+ @HostBinding('style.top.px')
+ positionTop: number;
+
+ @Input()
+ @HostBinding('style.left.px')
+ positionLeft: number;
+
+ /**
+ * Check that content is a TemplateRef.
+ *
+ * @return boolean
+ * */
+ get isTemplate(): boolean {
+ return this.content instanceof TemplateRef;
+ }
+
+ /**
+ * Check that content is an angular component.
+ *
+ * @return boolean
+ * */
+ get isComponent(): boolean {
+ return this.content instanceof Type;
+ }
+
+ /**
+ * Check that if content is not a TemplateRef or an angular component it means a primitive.
+ * */
+ get isPrimitive(): boolean {
+ return !this.isTemplate && !this.isComponent;
+ }
+}
diff --git a/src/framework/theme/components/popover/popover.directive.ts b/src/framework/theme/components/popover/popover.directive.ts
new file mode 100644
index 0000000000..f018625bd4
--- /dev/null
+++ b/src/framework/theme/components/popover/popover.directive.ts
@@ -0,0 +1,311 @@
+/**
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import { ComponentRef, Directive, ElementRef, HostListener, Input, OnDestroy, OnInit } from '@angular/core';
+import { NbPositioningHelper } from './helpers/positioning.helper';
+import { NbPopoverComponent, NbPopoverContent } from './popover.component';
+import { NbThemeService } from '../../services/theme.service';
+import { takeWhile } from 'rxjs/operators/takeWhile';
+import { NbAdjustmentHelper } from './helpers/adjustment.helper';
+import { NbTriggerHelper } from './helpers/trigger.helper';
+import { NbPopoverAdjustment, NbPopoverMode, NbPopoverPlacement, NbPopoverPosition } from './helpers/model';
+
+/**
+ * Powerful popover directive, which provides the best UX for your users.
+ *
+ * ![image](assets/images/components/popover.gif)
+ *
+ * @example Popover can accept different content such as:
+ * TemplateRef
+ *
+ * ```
+ *
+ *
+ * Hello, Popover!
+ *
+ * ```
+ *
+ * @example Custom components
+ *
+ * ```
+ *
+ * ```
+ *
+ * @example Primitive types
+ *
+ * ```
+ *
+ * ```
+ *
+ * @example Popover has different placements, such as: top, bottom, left and right
+ * which can be used as following:
+ *
+ * ```
+ *
+ * ```
+ *
+ * @example By default popover will try to adjust itself to maximally fit viewport
+ * and provide the best user experience. It will try to change placement of the popover container.
+ * If you wanna disable this behaviour just set it falsy value.
+ *
+ * ```
+ *
+ * ```
+ *
+ * */
+ /*
+ *
+ * TODO
+ * Rendering strategy have to be refactored.
+ * For now directive creates and deletes popover container each time.
+ * I think we can handle this slightly smarter and show/hide in any situations.
+ */
+@Directive({ selector: '[nbPopover]' })
+export class NbPopoverDirective implements OnInit, OnDestroy {
+
+ /**
+ * Popover content which will be rendered in NbPopoverComponent.
+ * Available content: template ref, component and any primitive.
+ * */
+ @Input('nbPopover')
+ content: NbPopoverContent;
+
+ /**
+ * Position will be calculated relatively host element based on the placement.
+ * Can be top, right, bottom and left.
+ * */
+ @Input('nbPopoverPlacement')
+ placement: NbPopoverPlacement = NbPopoverPlacement.TOP;
+
+ /**
+ * Container placement will be changes automatically based on this strategy if container can't fit view port.
+ * Set this property to any falsy value if you want to disable automatically adjustment.
+ * Available values: clockwise, counterclockwise.
+ * */
+ @Input('nbPopoverAdjustment')
+ adjustment: NbPopoverAdjustment = NbPopoverAdjustment.CLOCKWISE;
+
+ /**
+ * Describes when the container will be shown.
+ * Available options: click, hover and hint
+ * */
+ @Input('nbPopoverMode')
+ mode: NbPopoverMode = NbPopoverMode.CLICK;
+
+ /**
+ * Returns true if popover already shown.
+ * @return boolean
+ * */
+ get isShown(): boolean {
+ return !!this.containerRef;
+ }
+
+ /**
+ * Returns true if popover hidden.
+ * @return boolean
+ * */
+ get isHidden(): boolean {
+ return !this.containerRef;
+ }
+
+ /*
+ * Is used for unsubscribe all subscriptions after component destructuring.
+ * */
+ private alive: boolean = true;
+
+ private containerRef: ComponentRef;
+
+ private get container(): NbPopoverComponent {
+ return this.containerRef.instance;
+ }
+
+ private get containerElement(): HTMLElement {
+ return this.containerRef.location.nativeElement;
+ }
+
+ private get hostElement(): HTMLElement {
+ return this.hostRef.nativeElement;
+ }
+
+ constructor(private hostRef: ElementRef, private themeService: NbThemeService) {
+ }
+
+ ngOnInit() {
+ this.registerTriggers();
+ }
+
+ ngOnDestroy() {
+ this.alive = false;
+ }
+
+ /**
+ * Show popover.
+ * */
+ show() {
+ if (this.isHidden) {
+ this.renderPopover();
+ }
+ }
+
+ /**
+ * Hide popover.
+ * */
+ hide() {
+ if (this.isShown) {
+ this.destroyPopover();
+ }
+ }
+
+ /**
+ * Toggle popover state.
+ * */
+ toggle() {
+ if (this.isShown) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ }
+
+ /*
+ * Adjust popover position on window resize.
+ * Window resize may change host element position, so popover relocation required.
+ *
+ * TODO
+ * Fix tslint to add capability make HostListener private.
+ * */
+ @HostListener('window:resize', ['$event'])
+ onResize() {
+ if (this.isShown) {
+ this.place();
+ }
+ }
+
+ /*
+ * Subscribe to the popover triggers created from the {@link NbPopoverDirective#mode}.
+ * see {@link NbTriggerHelper}
+ * */
+ private registerTriggers() {
+ const { open, close, toggle } = NbTriggerHelper.createTrigger(this.hostElement, () => this.containerRef, this.mode);
+
+ open.pipe(takeWhile(() => this.alive))
+ .subscribe(() => this.show());
+
+ close.pipe(takeWhile(() => this.alive))
+ .subscribe(() => this.hide());
+
+ toggle.pipe(takeWhile(() => this.alive))
+ .subscribe(() => this.toggle());
+ }
+
+ /*
+ * Renders popover putting {@link NbPopoverComponent} in the top of {@link NbLayoutComponent}
+ * and positioning container based on {@link NbPopoverDirective#placement}
+ * and {@link NbPopoverDirective#adjustment}.
+ * */
+ private renderPopover() {
+ this.themeService.appendToLayoutTop(NbPopoverComponent)
+ .pipe(takeWhile(() => this.alive))
+ .subscribe((containerRef: ComponentRef) => {
+ this.containerRef = containerRef;
+ this.patchPopoverContent(this.content);
+ /*
+ * Have to call detectChanges because on this phase {@link NbPopoverComponent} isn't inserted in the DOM
+ * and haven't got calculated size.
+ * But we should have size on this step to calculate popover position correctly.
+ *
+ * TODO
+ * I don't think we have to call detectChanges each time we're using {@link NbThemeService#appendToLayoutTop}.
+ * Investigate, maybe we can create method in the {@link NbThemeService}
+ * which will call {@link NbThemeService#appendToLayoutTop} and 'do' detectChanges,
+ * instead of performing this call by service client.
+ * */
+ this.containerRef.changeDetectorRef.detectChanges();
+ this.place();
+ });
+ }
+
+ /*
+ * Destroys the {@link NbPopoverComponent} and nullify its reference;
+ * */
+ private destroyPopover() {
+ this.containerRef.destroy();
+ this.containerRef = null;
+ }
+
+ /*
+ * Moves {@link NbPopoverComponent} relatively host component based on the {@link NbPopoverDirective#placement}.
+ * */
+ private place() {
+ const hostRect = this.hostElement.getBoundingClientRect();
+ const containerRect = this.containerElement.getBoundingClientRect();
+
+ this.adjust(containerRect, hostRect);
+ }
+
+ /*
+ * Set container content.
+ * */
+ private patchPopoverContent(content: NbPopoverContent) {
+ this.container.content = content;
+ }
+
+ /*
+ * Set container placement.
+ * */
+ private patchPopoverPlacement(placement: NbPopoverPlacement) {
+ this.container.placement = placement;
+ }
+
+ /*
+ * Set container position.
+ * */
+ private patchPopoverPosition({ top: top, left: left }) {
+ this.container.positionTop = top;
+ this.container.positionLeft = left;
+ }
+
+ /*
+ * Calculates container adjustment and sets container position and placement.
+ * */
+ private adjust(containerRect: ClientRect, hostRect: ClientRect) {
+ const { placement, position } = this.performAdjustment(containerRect, hostRect);
+
+ this.patchPopoverPlacement(placement);
+ this.patchPopoverPosition(position);
+ }
+
+ /*
+ * Checks if {@link NbPopoverDirective#adjustment} can be performed and runs it.
+ * If not, just calculates element position.
+ * */
+ private performAdjustment(placed: ClientRect, host: ClientRect): NbPopoverPosition {
+ if (this.adjustment) {
+ return this.calcAdjustment(placed, host);
+ }
+
+ return this.calcPosition(placed, host);
+ }
+
+ /*
+ * Calculate adjustment.
+ * see {@link NbAdjustmentHelper}.
+ * */
+ private calcAdjustment(placed: ClientRect, host: ClientRect): NbPopoverPosition {
+ return NbAdjustmentHelper.adjust(placed, host, this.placement, this.adjustment)
+ }
+
+ /*
+ * Calculate position.
+ * see {@link NbPositioningHelper}
+ * */
+ private calcPosition(placed: ClientRect, host: ClientRect): NbPopoverPosition {
+ return {
+ position: NbPositioningHelper.calcPosition(placed, host, this.placement),
+ placement: this.placement,
+ }
+ }
+}
diff --git a/src/framework/theme/components/popover/popover.module.ts b/src/framework/theme/components/popover/popover.module.ts
new file mode 100644
index 0000000000..66ad5679b2
--- /dev/null
+++ b/src/framework/theme/components/popover/popover.module.ts
@@ -0,0 +1,19 @@
+/**
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import { NgModule } from '@angular/core';
+import { NbPopoverComponent } from './popover.component';
+import { NbSharedModule } from '../shared/shared.module';
+import { NbPopoverDirective } from './popover.directive';
+
+@NgModule({
+ imports: [NbSharedModule],
+ declarations: [NbPopoverComponent, NbPopoverDirective],
+ exports: [NbPopoverDirective],
+ entryComponents: [NbPopoverComponent],
+})
+export class NbPopoverModule {
+}
diff --git a/src/framework/theme/index.ts b/src/framework/theme/index.ts
index 4b66a6e559..cb4874258c 100644
--- a/src/framework/theme/index.ts
+++ b/src/framework/theme/index.ts
@@ -26,4 +26,5 @@ export * from './components/checkbox/checkbox.component';
export * from './components/checkbox/checkbox.module';
export * from './components/badge/badge.component';
export * from './components/badge/badge.module';
-
+export * from './components/popover/popover.directive';
+export * from './components/popover/popover.module';
diff --git a/src/framework/theme/styles/global/_components.scss b/src/framework/theme/styles/global/_components.scss
index 47d99d6252..6bfb0ed523 100644
--- a/src/framework/theme/styles/global/_components.scss
+++ b/src/framework/theme/styles/global/_components.scss
@@ -17,6 +17,7 @@
@import '../../components/search/search.component.theme';
@import '../../components/checkbox/checkbox.component.theme';
@import '../../components/badge/badge.component.theme';
+@import '../../components/popover/popover.component.theme';
@mixin nb-theme-components() {
@@ -33,4 +34,5 @@
@include nb-search-theme();
@include nb-checkbox-theme();
@include nb-badge-theme();
+ @include nb-popover-theme();
}
diff --git a/src/framework/theme/styles/themes/_cosmic.scss b/src/framework/theme/styles/themes/_cosmic.scss
index cd89c39b9c..31611eb81a 100644
--- a/src/framework/theme/styles/themes/_cosmic.scss
+++ b/src/framework/theme/styles/themes/_cosmic.scss
@@ -64,6 +64,9 @@ $theme: (
user-menu-active-bg: color-primary,
user-menu-border: color-primary,
+ popover-border: color-primary,
+ popover-shadow: shadow,
+
footer-height: header-height,
sidebar-width: 16.25rem,
diff --git a/src/framework/theme/styles/themes/_default.scss b/src/framework/theme/styles/themes/_default.scss
index 714ab54dd9..02b4cadb3a 100644
--- a/src/framework/theme/styles/themes/_default.scss
+++ b/src/framework/theme/styles/themes/_default.scss
@@ -232,6 +232,11 @@ $theme: (
user-menu-active-bg: color-success,
user-menu-border: color-success,
+ popover-fg: color-fg-heading,
+ popover-bg: color-bg,
+ popover-border: color-success,
+ popover-shadow: none,
+
actions-font-size: font-size,
actions-font-family: font-secondary,
actions-line-height: line-height,