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 + + + + + + + + + + + + + + + + + + + + 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,