diff --git a/e2e/context-menu.e2e-spec.ts b/e2e/context-menu.e2e-spec.ts index 6494cba35e..4cc9a9dfff 100644 --- a/e2e/context-menu.e2e-spec.ts +++ b/e2e/context-menu.e2e-spec.ts @@ -1,7 +1,7 @@ import { browser, by, element } from 'protractor'; const withContextMenu = by.css('nb-card:nth-child(1) nb-user:nth-child(1)'); -const popover = by.css('nb-layout > nb-popover'); +const popover = by.css('nb-layout nb-context-menu'); describe('nb-context-menu', () => { diff --git a/e2e/layout-dynamic.e2e-spec.ts b/e2e/layout-dynamic.e2e-spec.ts deleted file mode 100644 index f1297dcf86..0000000000 --- a/e2e/layout-dynamic.e2e-spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @license - * Copyright Akveo. All Rights Reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -import { browser, element, by } from 'protractor'; - -describe('nb-layout theme', () => { - - beforeEach((done) => { - browser.get('#/layout/theme-dynamic-test.component').then(() => done()); - }); - - it('shown have layout first', () => { - element(by.css('nb-layout')).$$('*').first().getTagName().then(value => { - expect(value).toMatch('div'); - }); - }); - - it('should append into nb-layout', () => { - - const button = element(by.css('#add-dynamic')); - - button.click().then(() => { - return browser.driver.wait(() => { - return element(by.css('nb-layout')).$$('*').first().getTagName().then(value => { - return value === 'nb-dynamic-to-add'; - }); - }, 10000); - }); - - element(by.css('nb-layout')).$$('*').first().getTagName().then(value => { - expect(value).toMatch('nb-dynamic-to-add'); - }); - element(by.css('nb-layout')).$$('*').get(1).getTagName().then(value => { - expect(value).toMatch('div'); - }); - }); - - it('should append by factory into nb-layout', () => { - - const button = element(by.css('#add-dynamic-by-factory')); - - button.click().then(() => { - return browser.driver.wait(() => { - return element(by.css('nb-layout')).$$('*').first().getTagName().then(value => { - return value === 'nb-dynamic-to-add'; - }); - }, 10000); - }); - - element(by.css('nb-layout')).$$('*').first().getTagName().then(value => { - expect(value).toMatch('nb-dynamic-to-add'); - }); - element(by.css('nb-layout')).$$('*').get(1).getTagName().then(value => { - expect(value).toMatch('div'); - }); - }); - - it('should clear dymamic nb-layout area', () => { - - const buttonAdd = element(by.css('#add-dynamic')); - const buttonClear = element(by.css('#clear-dynamic')); - - buttonAdd.click().then(() => { - return browser.driver.wait(() => { - return element(by.css('nb-layout')).$$('*').first().getTagName().then(value => { - return value === 'nb-dynamic-to-add'; - }); - }, 10000); - }); - element(by.css('nb-layout')).$$('*').first().getTagName().then(value => { - expect(value).toMatch('nb-dynamic-to-add'); - }); - - buttonClear.click().then(() => { - return browser.driver.wait(() => { - return element(by.css('nb-layout')).$$('*').first().getTagName().then(value => { - return value === 'div'; - }); - }, 10000); - }); - - element(by.css('nb-layout')).$$('*').first().getTagName().then(value => { - expect(value).toMatch('div'); - }); - }); - -}); diff --git a/e2e/popover.e2e-spec.ts b/e2e/popover.e2e-spec.ts index 69e58750ff..773764c3d8 100644 --- a/e2e/popover.e2e-spec.ts +++ b/e2e/popover.e2e-spec.ts @@ -11,7 +11,7 @@ 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'); +const popover = by.css('nb-layout nb-popover'); describe('nb-popover', () => { @@ -35,7 +35,7 @@ describe('nb-popover', () => { element(contentString).click(); const containerContent = element(popover).element(by.css('div')); expect(containerContent.isPresent()).toBeTruthy(); - expect(containerContent.getAttribute('class')).toEqual('primitive-popover'); + expect(containerContent.getAttribute('class')).toEqual('primitive-overlay'); expect(containerContent.getText()).toEqual('Hi, I\'m popover!'); }); @@ -49,28 +49,28 @@ describe('nb-popover', () => { element(placementRight).click(); const container = element(popover); expect(container.isPresent()).toBeTruthy(); - expect(container.getAttribute('class')).toEqual('right'); + expect(container.getAttribute('class')).toEqual('nb-overlay-right'); }); it('render container in the bottom', () => { element(placementBottom).click(); const container = element(popover); expect(container.isPresent()).toBeTruthy(); - expect(container.getAttribute('class')).toEqual('bottom'); + expect(container.getAttribute('class')).toEqual('nb-overlay-bottom'); }); it('render container in the top', () => { element(placementTop).click(); const container = element(popover); expect(container.isPresent()).toBeTruthy(); - expect(container.getAttribute('class')).toEqual('top'); + expect(container.getAttribute('class')).toEqual('nb-overlay-top'); }); it('render container in the left', () => { element(placementLeft).click(); const container = element(popover); expect(container.isPresent()).toBeTruthy(); - expect(container.getAttribute('class')).toEqual('left'); + expect(container.getAttribute('class')).toEqual('nb-overlay-left'); }); it('open popover by host click', () => { diff --git a/e2e/search.e2e-spec.ts b/e2e/search.e2e-spec.ts index 9e1a238c8b..81f8c4bb4e 100644 --- a/e2e/search.e2e-spec.ts +++ b/e2e/search.e2e-spec.ts @@ -9,6 +9,7 @@ import { hasClass } from './e2e-helper'; import { protractor } from 'protractor/built/ptor'; const EC = protractor.ExpectedConditions; +const WAIT_TIME = 500; describe('nb-search', () => { @@ -18,47 +19,86 @@ describe('nb-search', () => { it('should be able to show search-field', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); expect(hasClass(element(by.css('nb-search-field')), 'show')).toBeTruthy(); }); it('should be able to change layout style', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); expect(hasClass(element(by.css('nb-layout')), 'with-search')).toBeTruthy(); }); it('should focus on opened search field', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); expect(hasClass(browser.driver.switchTo().activeElement(), 'search-input')).toBeTruthy(); }); it('should be able to close search-field with close button', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); element(by.css('.search button')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.start-search'))), WAIT_TIME); expect(hasClass(element(by.css('nb-search-field')), 'show')).toBeFalsy(); }); it('should remove class from layout when search closed', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); element(by.css('.search button')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.start-search'))), WAIT_TIME); expect(hasClass(element(by.css('nb-layout')), 'with-search')).toBeFalsy(); }); it('should remove focus from input when search closed', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); element(by.css('.search button')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.start-search'))), WAIT_TIME); expect(hasClass(browser.driver.switchTo().activeElement(), 'search-input')).toBeFalsy(); }); it('should clean search input when closed', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); element(by.css('.search-input')).sendKeys('akveo'); element(by.css('.search button')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.start-search'))), WAIT_TIME); expect(element(by.css('.search-input')).getAttribute('value')).toEqual(''); }); it('should be able to close search-field with esc', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); element(by.css('.search-input')).sendKeys(protractor.Key.ESCAPE); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.start-search'))), WAIT_TIME); expect(hasClass(element(by.css('nb-search-field')), 'show')).toBeFalsy(); expect(hasClass(element(by.css('nb-layout')), 'with-search')).toBeFalsy(); expect(hasClass(browser.driver.switchTo().activeElement(), 'search-input')).toBeFalsy(); @@ -67,8 +107,14 @@ describe('nb-search', () => { it('should be able to submit search and close search-field with enter', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); element(by.css('.search-input')).sendKeys('akveo'); element(by.css('.search-input')).sendKeys(protractor.Key.ENTER); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.start-search'))), WAIT_TIME); expect(hasClass(element(by.css('nb-search-field')), 'show')).toBeFalsy(); expect(hasClass(element(by.css('nb-layout')), 'with-search')).toBeFalsy(); expect(hasClass(browser.driver.switchTo().activeElement(), 'search-input')).toBeFalsy(); @@ -77,6 +123,9 @@ describe('nb-search', () => { it('should display default hint', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); expect(element(by.css('.show .search span'))).toBeTruthy(); const spanEl = element(by.css('.show .search span')); @@ -87,6 +136,9 @@ describe('nb-search', () => { it('should display default placeholder', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); expect(element(by.css('.search-input')).getAttribute('placeholder')).toEqual('Search...'); }); }); @@ -99,6 +151,9 @@ describe('nb-search-customized', () => { it('should display customised hint', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); expect(element(by.css('.show .search span'))).toBeTruthy(); const spanEl = element(by.css('.show .search span')); @@ -109,6 +164,9 @@ describe('nb-search-customized', () => { it('should display customised placeholder', () => { element(by.css('.start-search')).click(); + // TODO: Remove after implementing search animations with angular. + // For now need to wait animation to complete before performing checks. + browser.wait(EC.visibilityOf(element(by.css('.search-input'))), WAIT_TIME); expect(element(by.css('.search-input')).getAttribute('placeholder')).toEqual('Type here.'); }); }); diff --git a/src/framework/theme/components/cdk/adapter/adapter.module.ts b/src/framework/theme/components/cdk/adapter/adapter.module.ts index 1981676881..6a28a37339 100644 --- a/src/framework/theme/components/cdk/adapter/adapter.module.ts +++ b/src/framework/theme/components/cdk/adapter/adapter.module.ts @@ -9,7 +9,8 @@ import { NbViewportRulerAdapter } from './viewport-ruler-adapter'; @NgModule({ providers: [ NbViewportRulerAdapter, - { provide: OverlayContainer, useClass: NbOverlayContainerAdapter }, + NbOverlayContainerAdapter, + { provide: OverlayContainer, useExisting: NbOverlayContainerAdapter }, { provide: ScrollDispatcher, useClass: NbScrollDispatcherAdapter }, ], }) diff --git a/src/framework/theme/components/cdk/adapter/overlay-container-adapter.ts b/src/framework/theme/components/cdk/adapter/overlay-container-adapter.ts index 169975896e..0ad4eba0d7 100644 --- a/src/framework/theme/components/cdk/adapter/overlay-container-adapter.ts +++ b/src/framework/theme/components/cdk/adapter/overlay-container-adapter.ts @@ -3,13 +3,31 @@ import { Injectable } from '@angular/core'; import { NbOverlayContainer } from '../overlay/mapping'; +/** + * Provides nb-layout as overlay container. + * Container has to be cleared when layout destroys. + * Another way previous version of the container will be used + * but it isn't inserted in DOM and exists in memory only. + * This case important only if you switch between multiple layouts. + * */ @Injectable() export class NbOverlayContainerAdapter extends NbOverlayContainer { + protected container: HTMLElement; + + setContainer(container: HTMLElement) { + this.container = container; + } + + clearContainer() { + this.container = null; + this._containerElement = null; + } + protected _createContainer(): void { const container = this._document.createElement('div'); container.classList.add('cdk-overlay-container'); - this._document.querySelector('nb-layout').appendChild(container); + this.container.appendChild(container); this._containerElement = container; } } diff --git a/src/framework/theme/components/cdk/overlay/overlay-trigger.ts b/src/framework/theme/components/cdk/overlay/overlay-trigger.ts index 3a0605948d..d15adf6413 100644 --- a/src/framework/theme/components/cdk/overlay/overlay-trigger.ts +++ b/src/framework/theme/components/cdk/overlay/overlay-trigger.ts @@ -14,6 +14,10 @@ export enum NbTrigger { * Each stream provides different events depends on implementation. * We have three main trigger strategies: click, hint and hover. * */ +/** + * TODO maybe we have to use renderer.listen instead of observableFromEvent? + * Renderer provides capability use it in service worker, ssr and so on. + * */ export abstract class NbTriggerStrategy { abstract show$: Observable; abstract hide$: Observable; diff --git a/src/framework/theme/components/context-menu/_context-menu.component.theme.scss b/src/framework/theme/components/context-menu/_context-menu.component.theme.scss index fb3497cec3..c0d4eab0ad 100644 --- a/src/framework/theme/components/context-menu/_context-menu.component.theme.scss +++ b/src/framework/theme/components/context-menu/_context-menu.component.theme.scss @@ -3,10 +3,64 @@ * Copyright Akveo. All Rights Reserved. * Licensed under the MIT License. See License.txt in the project root for license information. */ - @import '../../styles/core/mixins'; @mixin nb-context-menu-theme() { + nb-context-menu { + $arrow-size: nb-theme(context-menu-arrow-size); + $arrow-content-size: calc(#{$arrow-size} - 2px); + + border: 2px solid nb-theme(context-menu-border); + border-radius: nb-theme(context-menu-border-radius); + background: nb-theme(context-menu-bg); + box-shadow: nb-theme(context-menu-shadow); + + .primitive-overlay { + color: nb-theme(context-menu-fg); + } + + .arrow { + border-left: $arrow-size solid transparent; + border-right: $arrow-size solid transparent; + border-bottom: $arrow-size solid nb-theme(context-menu-border); + + &::after { + position: absolute; + content: ' '; + width: 0; + height: 0; + top: 3px; + left: calc(50% - #{$arrow-content-size}); + border-left: $arrow-content-size solid transparent; + border-right: $arrow-content-size solid transparent; + border-bottom: $arrow-content-size solid nb-theme(context-menu-bg); + } + } + + &.nb-overlay-bottom .arrow { + top: calc(-#{$arrow-size} + 1px); + left: calc(50% - #{$arrow-size}); + } + + &.nb-overlay-left .arrow { + right: round(-$arrow-size - $arrow-size / 2 + 2px); + top: calc(50% - #{$arrow-size / 2}); + transform: rotate(90deg); + } + + &.nb-overlay-top .arrow { + bottom: calc(-#{$arrow-size} + 1px); + left: calc(50% - #{$arrow-size}); + transform: rotate(180deg); + } + + &.nb-overlay-right .arrow { + left: round(-$arrow-size - $arrow-size / 2 + 2px); + top: calc(50% - #{$arrow-size / 2}); + transform: rotate(270deg); + } + } + nb-menu.context-menu .menu-items .menu-item a { color: nb-theme(context-menu-fg); font-weight: nb-theme(font-weight-normal); diff --git a/src/framework/theme/components/context-menu/context-menu.component.scss b/src/framework/theme/components/context-menu/context-menu.component.scss index 721139813c..6f9feab91f 100644 --- a/src/framework/theme/components/context-menu/context-menu.component.scss +++ b/src/framework/theme/components/context-menu/context-menu.component.scss @@ -3,49 +3,57 @@ * Copyright Akveo. All Rights Reserved. * Licensed under the MIT License. See License.txt in the project root for license information. */ - @import '../../styles/core/mixins'; -:host /deep/ nb-menu { - display: inline; - font-size: 0.875rem; - line-height: 1.5rem; - - ul.menu-items { - margin: 0; - padding: 0.5rem 0; - - .menu-item { - border: none; - white-space: nowrap; +:host { + .arrow { + position: absolute; - &:first-child { - border: none; - } + width: 0; + height: 0; + } - a { - cursor: pointer; - border-radius: 0; - padding: 0; + /deep/ nb-menu { + display: inline; + font-size: 0.875rem; + line-height: 1.5rem; - .menu-icon { - font-size: 1.5rem; - width: auto; - } + ul.menu-items { + margin: 0; + padding: 0.5rem 0; - .menu-title { - padding: 0.375rem 3rem; - @include nb-rtl(text-align, right); - } + .menu-item { + border: none; + white-space: nowrap; - .menu-icon ~ .menu-title { - @include nb-ltr(padding-left, 0); - @include nb-rtl(padding-right, 0); + &:first-child { + border: none; } - .menu-icon:first-child { - @include nb-ltr(padding-left, 1rem); - @include nb-rtl(padding-right, 1rem); + a { + cursor: pointer; + border-radius: 0; + padding: 0; + + .menu-icon { + font-size: 1.5rem; + width: auto; + } + + .menu-title { + padding: 0.375rem 3rem; + @include nb-rtl(text-align, right); + } + + .menu-icon ~ .menu-title { + @include nb-ltr(padding-left, 0); + @include nb-rtl(padding-right, 0); + } + + .menu-icon:first-child { + @include nb-ltr(padding-left, 1rem); + @include nb-rtl(padding-right, 1rem); + } } } } diff --git a/src/framework/theme/components/context-menu/context-menu.component.ts b/src/framework/theme/components/context-menu/context-menu.component.ts index 4c78f20ddc..c3d9f603ac 100644 --- a/src/framework/theme/components/context-menu/context-menu.component.ts +++ b/src/framework/theme/components/context-menu/context-menu.component.ts @@ -5,7 +5,9 @@ */ import { Component, Input } from '@angular/core'; -import { NbMenuItem } from '../../'; + +import { NbMenuItem } from '../../components/menu/menu.service'; +import { NbPositionedContainer } from '../cdk'; /** * Context menu component used as content within NbContextMenuDirective. @@ -19,13 +21,12 @@ import { NbMenuItem } from '../../'; @Component({ selector: 'nb-context-menu', styleUrls: ['./context-menu.component.scss'], - template: '', + template: ` + + + `, }) -export class NbContextMenuComponent { - - @Input() - items: NbMenuItem[] = []; - - @Input() - tag: string; +export class NbContextMenuComponent extends NbPositionedContainer { + @Input() items: NbMenuItem[] = []; + @Input() tag: string; } diff --git a/src/framework/theme/components/context-menu/context-menu.directive.ts b/src/framework/theme/components/context-menu/context-menu.directive.ts index 863bb2b1b9..286119cd0a 100644 --- a/src/framework/theme/components/context-menu/context-menu.directive.ts +++ b/src/framework/theme/components/context-menu/context-menu.directive.ts @@ -4,20 +4,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { - ComponentFactoryResolver, Directive, ElementRef, HostListener, Inject, Input, OnDestroy, - OnInit, PLATFORM_ID, -} from '@angular/core'; +import { AfterViewInit, ComponentRef, Directive, ElementRef, Inject, Input, OnDestroy } from '@angular/core'; import { filter, takeWhile } from 'rxjs/operators'; -import { NbPopoverDirective } from '../popover/popover.directive'; -import { NbMenuItem, NbMenuService } from '../menu/menu.service'; -import { NbThemeService } from '../../services/theme.service'; -import { NbPopoverAdjustment, NbPopoverPlacement } from '../popover/helpers/model'; + +import { + createContainer, + NbAdjustableConnectedPositionStrategy, + NbAdjustment, + NbOverlayRef, + NbOverlayService, + NbPosition, + NbPositionBuilderService, + NbTrigger, + NbTriggerStrategy, + NbTriggerStrategyBuilder, + patch, +} from '../cdk'; import { NbContextMenuComponent } from './context-menu.component'; -import { NbPositioningHelper } from '../popover/helpers/positioning.helper'; -import { NbAdjustmentHelper } from '../popover/helpers/adjustment.helper'; -import { NbTriggerHelper } from '../popover/helpers/trigger.helper'; -import { NbPlacementHelper } from '../popover/helpers/placement.helper'; +import { NbMenuItem, NbMenuService } from '../menu/menu.service'; +import { NB_DOCUMENT } from '../../theme.options'; /** * Full featured context menu directive. @@ -33,7 +38,7 @@ import { NbPlacementHelper } from '../popover/helpers/placement.helper'; * ``` * * If you want to handle context menu clicks you have to pass `nbContextMenuTag` - * param and subscribe to events using NbMenuService. + * param and register to events using NbMenuService. * `NbContextMenu` renders plain `NbMenu` inside, so * you have to work with it just like with `NbMenu` component: * @@ -51,7 +56,7 @@ import { NbPlacementHelper } from '../popover/helpers/placement.helper'; * ``` * * By default context menu will try to adjust itself to maximally fit viewport - * and provide the best user experience. It will try to change placement of the context menu. + * and provide the best user experience. It will try to change position of the context menu. * If you wanna disable this behaviour just set it falsy value. * * ```html @@ -63,110 +68,116 @@ import { NbPlacementHelper } from '../popover/helpers/placement.helper'; * ``` * */ @Directive({ selector: '[nbContextMenu]' }) -export class NbContextMenuDirective implements OnInit, OnDestroy { - - /** - * Basic menu items, will be passed to the internal NbMenuComponent. - * */ - @Input('nbContextMenu') - set items(items: NbMenuItem[]) { - this.validateItems(items); - this.popover.context = Object.assign(this.context, { items }); - }; +export class NbContextMenuDirective implements AfterViewInit, OnDestroy { /** - * Position will be calculated relatively host element based on the placement. + * Position will be calculated relatively host element based on the position. * Can be top, right, bottom and left. * */ @Input('nbContextMenuPlacement') - set placement(placement: NbPopoverPlacement) { - this.popover.placement = placement; - }; + position: NbPosition = NbPosition.BOTTOM; /** - * Container placement will be changes automatically based on this strategy if container can't fit view port. + * Container position 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('nbContextMenuAdjustment') - set adjustment(adjustment: NbPopoverAdjustment) { - this.popover.adjustment = adjustment; - } + adjustment: NbAdjustment = NbAdjustment.CLOCKWISE; /** * Set NbMenu tag, which helps identify menu when working with NbMenuService. * */ @Input('nbContextMenuTag') - set tag(tag: string) { - this.menuTag = tag; - this.popover.context = Object.assign(this.context, { tag }); - } + tag: string; + + /** + * Basic menu items, will be passed to the internal NbMenuComponent. + * */ + @Input('nbContextMenu') + set setItems(items: NbMenuItem[]) { + this.validateItems(items); + this.items = items; + }; - protected popover: NbPopoverDirective; - protected context = {}; - - private menuTag: string; - private alive: boolean = true; - - constructor(hostRef: ElementRef, - themeService: NbThemeService, - componentFactoryResolver: ComponentFactoryResolver, - positioningHelper: NbPositioningHelper, - adjustmentHelper: NbAdjustmentHelper, - triggerHelper: NbTriggerHelper, - @Inject(PLATFORM_ID) platformId, - placementHelper: NbPlacementHelper, - private menuService: NbMenuService) { - /** - * Initialize popover with all the important inputs. - * */ - this.popover = new NbPopoverDirective(hostRef, - themeService, - componentFactoryResolver, - positioningHelper, - adjustmentHelper, - triggerHelper, - platformId, - placementHelper, - ); - this.popover.content = NbContextMenuComponent; - this.popover.placement = NbPopoverPlacement.BOTTOM; + protected ref: NbOverlayRef; + protected container: ComponentRef; + protected positionStrategy: NbAdjustableConnectedPositionStrategy; + protected triggerStrategy: NbTriggerStrategy; + protected alive: boolean = true; + private items: NbMenuItem[] = []; + + constructor(@Inject(NB_DOCUMENT) protected document, + private menuService: NbMenuService, + private hostRef: ElementRef, + private positionBuilder: NbPositionBuilderService, + private overlay: NbOverlayService) { } - ngOnInit() { - this.popover.ngOnInit(); + ngAfterViewInit() { + this.positionStrategy = this.createPositionStrategy(); + this.ref = this.overlay.create({ + positionStrategy: this.positionStrategy, + scrollStrategy: this.overlay.scrollStrategies.reposition(), + }); + this.triggerStrategy = this.createTriggerStrategy(); + + this.subscribeOnTriggers(); + this.subscribeOnPositionChange(); this.subscribeOnItemClick(); } ngOnDestroy() { - this.popover.ngOnDestroy(); this.alive = false; + this.hide(); } - /** - * Show context menu. - * */ show() { - this.popover.show(); + this.container = createContainer(this.ref, NbContextMenuComponent, { + position: this.position, + items: this.items, + tag: this.tag, + }); } - /** - * Hide context menu. - * */ hide() { - this.popover.hide(); + this.ref.detach(); + this.container = null; } - /** - * Toggle context menu state. - * */ toggle() { - this.popover.toggle(); + if (this.ref && this.ref.hasAttached()) { + this.hide(); + } else { + this.show(); + } + } + + protected createPositionStrategy(): NbAdjustableConnectedPositionStrategy { + return this.positionBuilder + .connectedTo(this.hostRef) + .position(this.position) + .adjustment(this.adjustment); + } + + protected createTriggerStrategy(): NbTriggerStrategy { + return new NbTriggerStrategyBuilder() + .document(this.document) + .trigger(NbTrigger.CLICK) + .host(this.hostRef.nativeElement) + .container(() => this.container) + .build(); + } + + protected subscribeOnPositionChange() { + this.positionStrategy.positionChange + .pipe(takeWhile(() => this.alive)) + .subscribe((position: NbPosition) => patch(this.container, { position })); } - @HostListener('window:resize', ['$event']) - onResize() { - this.popover.onResize(); + protected subscribeOnTriggers() { + this.triggerStrategy.show$.pipe(takeWhile(() => this.alive)).subscribe(() => this.show()); + this.triggerStrategy.hide$.pipe(takeWhile(() => this.alive)).subscribe(() => this.hide()); } /* @@ -183,7 +194,7 @@ export class NbContextMenuDirective implements OnInit, OnDestroy { this.menuService.onItemClick() .pipe( takeWhile(() => this.alive), - filter(({tag}) => tag === this.menuTag), + filter(({ tag }) => tag === this.tag), ) .subscribe(() => this.hide()); } diff --git a/src/framework/theme/components/context-menu/context-menu.module.ts b/src/framework/theme/components/context-menu/context-menu.module.ts index 4199df9f02..09cdeb0276 100644 --- a/src/framework/theme/components/context-menu/context-menu.module.ts +++ b/src/framework/theme/components/context-menu/context-menu.module.ts @@ -6,17 +6,18 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; + +import { NbOverlayModule } from '../cdk'; import { NbContextMenuDirective } from './context-menu.directive'; import { NbContextMenuComponent } from './context-menu.component'; import { NbMenuModule } from '../menu/menu.module'; -import { NbPopoverComponent } from '../popover/popover.component'; -import { NbPopoverModule } from '../popover/popover.module'; + @NgModule({ - imports: [CommonModule, NbPopoverModule, NbMenuModule], + imports: [CommonModule, NbOverlayModule, NbMenuModule], exports: [NbContextMenuDirective], declarations: [NbContextMenuDirective, NbContextMenuComponent], - entryComponents: [NbPopoverComponent, NbContextMenuComponent], + entryComponents: [NbContextMenuComponent], }) export class NbContextMenuModule { } diff --git a/src/framework/theme/components/layout/layout.component.ts b/src/framework/theme/components/layout/layout.component.ts index 458a845078..b01e9069b4 100644 --- a/src/framework/theme/components/layout/layout.component.ts +++ b/src/framework/theme/components/layout/layout.component.ts @@ -5,12 +5,11 @@ */ import { - AfterViewInit, Component, ComponentFactoryResolver, ElementRef, HostBinding, HostListener, Input, OnDestroy, - Renderer2, ViewChild, ViewContainerRef, ComponentFactory, Inject, PLATFORM_ID, forwardRef, + AfterViewInit, Component, ElementRef, HostBinding, HostListener, Input, OnDestroy, + Renderer2, ViewChild, ViewContainerRef, Inject, PLATFORM_ID, forwardRef, } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; -import { Router } from '@angular/router'; -import { Subject, BehaviorSubject } from 'rxjs'; +import { BehaviorSubject } from 'rxjs'; import { filter, takeWhile } from 'rxjs/operators'; import { convertToBoolProperty } from '../helpers'; @@ -21,6 +20,7 @@ import { NbRestoreScrollTopHelper } from './restore-scroll-top.service'; import { NbScrollPosition, NbLayoutScrollService } from '../../services/scroll.service'; import { NbLayoutDimensions, NbLayoutRulerService } from '../../services/ruler.service'; import { NB_WINDOW, NB_DOCUMENT } from '../../theme.options'; +import { NbOverlayContainerAdapter } from '../cdk/adapter/overlay-container-adapter'; /** * A container component which determines a content position inside of the layout. @@ -250,7 +250,6 @@ export class NbLayoutFooterComponent { selector: 'nb-layout', styleUrls: ['./layout.component.scss'], template: ` -
@@ -337,10 +336,8 @@ export class NbLayoutComponent implements AfterViewInit, OnDestroy { constructor( protected themeService: NbThemeService, protected spinnerService: NbSpinnerService, - protected componentFactoryResolver: ComponentFactoryResolver, protected elementRef: ElementRef, protected renderer: Renderer2, - protected router: Router, @Inject(NB_WINDOW) protected window, @Inject(NB_DOCUMENT) protected document, @Inject(PLATFORM_ID) protected platformId: Object, @@ -348,7 +345,9 @@ export class NbLayoutComponent implements AfterViewInit, OnDestroy { protected scrollService: NbLayoutScrollService, protected rulerService: NbLayoutRulerService, protected scrollTop: NbRestoreScrollTopHelper, + protected overlayContainer: NbOverlayContainerAdapter, ) { + this.registerAsOverlayContainer(); this.themeService.onThemeChange() .pipe( @@ -422,25 +421,6 @@ export class NbLayoutComponent implements AfterViewInit, OnDestroy { } ngAfterViewInit() { - this.themeService.onAppendToTop() - .pipe( - takeWhile(() => this.alive), - ) - .subscribe((data: { factory: ComponentFactory, listener: Subject }) => { - const componentRef = this.veryTopRef.createComponent(data.factory); - data.listener.next(componentRef); - data.listener.complete(); - }); - - this.themeService.onClearLayoutTop() - .pipe( - takeWhile(() => this.alive), - ) - .subscribe((data: { listener: Subject }) => { - this.veryTopRef.clear(); - data.listener.next(true); - }); - this.layoutDirectionService.onDirectionChange() .pipe(takeWhile(() => this.alive)) .subscribe(direction => { @@ -455,8 +435,8 @@ export class NbLayoutComponent implements AfterViewInit, OnDestroy { } ngOnDestroy() { - this.themeService.clearLayoutTop(); this.alive = false; + this.unregisterAsOverlayContainer(); } @HostListener('window:scroll', ['$event']) @@ -526,6 +506,18 @@ export class NbLayoutComponent implements AfterViewInit, OnDestroy { return { x, y }; } + protected registerAsOverlayContainer() { + if (this.overlayContainer.setContainer) { + this.overlayContainer.setContainer(this.elementRef.nativeElement); + } + } + + protected unregisterAsOverlayContainer() { + if (this.overlayContainer.clearContainer) { + this.overlayContainer.clearContainer(); + } + } + private scroll(x: number = null, y: number = null) { const { x: currentX, y: currentY } = this.getScrollPosition(); x = x == null ? currentX : x; diff --git a/src/framework/theme/components/popover/_popover.component.theme.scss b/src/framework/theme/components/popover/_popover.component.theme.scss index fc950453f3..e3caed4d5f 100644 --- a/src/framework/theme/components/popover/_popover.component.theme.scss +++ b/src/framework/theme/components/popover/_popover.component.theme.scss @@ -6,22 +6,58 @@ @mixin nb-popover-theme { nb-popover { - $arrow-size: 11px; - $arrow-content-size: 9px; + $arrow-size: nb-theme(popover-arrow-size); + $arrow-content-size: calc(#{$arrow-size} - 2px); + border: 2px solid nb-theme(popover-border); + border-radius: nb-theme(popover-border-radius); background: nb-theme(popover-bg); box-shadow: nb-theme(popover-shadow); - .primitive-popover { + .primitive-overlay { color: nb-theme(popover-fg); } .arrow { + border-left: $arrow-size solid transparent; + border-right: $arrow-size solid transparent; border-bottom: $arrow-size solid nb-theme(popover-border); + &::after { + position: absolute; + content: ' '; + width: 0; + height: 0; + top: 3px; + left: calc(50% - #{$arrow-content-size}); + border-left: $arrow-content-size solid transparent; + border-right: $arrow-content-size solid transparent; border-bottom: $arrow-content-size solid nb-theme(popover-bg); } } + + &.nb-overlay-bottom .arrow { + top: calc(-#{$arrow-size} + 1px); + left: calc(50% - #{$arrow-size}); + } + + &.nb-overlay-left .arrow { + right: round(-$arrow-size - $arrow-size / 2 + 2px); + top: calc(50% - #{$arrow-size / 2}); + transform: rotate(90deg); + } + + &.nb-overlay-top .arrow { + bottom: calc(-#{$arrow-size} + 1px); + left: calc(50% - #{$arrow-size}); + transform: rotate(180deg); + } + + &.nb-overlay-right .arrow { + left: round(-$arrow-size - $arrow-size / 2 + 2px); + top: calc(50% - #{$arrow-size / 2}); + transform: rotate(270deg); + } } } diff --git a/src/framework/theme/components/popover/helpers/adjustment.helper.ts b/src/framework/theme/components/popover/helpers/adjustment.helper.ts deleted file mode 100644 index 5c102b6d8b..0000000000 --- a/src/framework/theme/components/popover/helpers/adjustment.helper.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Injectable, Inject } from '@angular/core'; - -import { NB_WINDOW } from '../../../theme.options'; -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, - ], -}; - -@Injectable() -export class NbAdjustmentHelper { - - private window: Window; - - constructor( - private positioningHelper: NbPositioningHelper, - @Inject(NB_WINDOW) window) { - this.window = window as Window; - } - - /** - * 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. - * */ - adjust(placed: ClientRect, - host: ClientRect, - placement: NbPopoverPlacement, - adjustment: NbPopoverAdjustment): NbPopoverPosition { - const placements = NB_ORDERED_PLACEMENTS[adjustment].slice(); - const ordered = this.orderPlacements(placement, placements); - const possible = ordered.map(pl => ({ - position: this.positioningHelper.calcPosition(placed, host, pl), - placement: pl, - })); - - return this.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 chooseBest(placed: ClientRect, possible: NbPopoverPosition[]): NbPopoverPosition { - return possible.find(adjust => this.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 inViewPort(placed: ClientRect, position: NbPopoverPosition): boolean { - return position.position.top - this.window.pageYOffset > 0 - && position.position.left - this.window.pageXOffset > 0 - && position.position.top + placed.height < this.window.innerHeight + this.window.pageYOffset - && position.position.left + placed.width < this.window.innerWidth + this.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. - * - * order placements for {@link NbPopoverPlacement#RIGHT} and {@link NbPopoverAdjustment#CLOCKWISE} - * ```ts - * 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 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 deleted file mode 100644 index e122b76619..0000000000 --- a/src/framework/theme/components/popover/helpers/adjustment.spec.ts +++ /dev/null @@ -1,245 +0,0 @@ -/** - * @license - * Copyright Akveo. All Rights Reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ -import { async, inject, TestBed } from '@angular/core/testing'; - -import { NbAdjustmentHelper } from './adjustment.helper'; -import { NbPopoverAdjustment, NbPopoverPlacement } from './model'; -import { NB_DOCUMENT, NB_WINDOW } from '../../../theme.options'; -import { NbPositioningHelper } from './positioning.helper'; - -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, - }, - }; - - let adjustmentHelper: NbAdjustmentHelper; - - beforeEach(() => { - // Configure testbed to prepare services - TestBed.configureTestingModule({ - providers: [ - { provide: NB_WINDOW, useValue: window }, - { provide: NB_DOCUMENT, useValue: document }, - NbPositioningHelper, - NbAdjustmentHelper, - ], - }); - }); - - // Single async inject to save references; which are used in all tests below - beforeEach(async(inject( - [NbAdjustmentHelper], - (_adjustmentHelper) => { - adjustmentHelper = _adjustmentHelper - }, - ))); - - 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 = adjustmentHelper.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 = adjustmentHelper.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 = adjustmentHelper.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 = adjustmentHelper.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 = adjustmentHelper.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 = adjustmentHelper.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 = adjustmentHelper.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 = adjustmentHelper.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 = adjustmentHelper.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 = adjustmentHelper.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 = adjustmentHelper.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 = adjustmentHelper.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 deleted file mode 100644 index 1d7d9aef2a..0000000000 --- a/src/framework/theme/components/popover/helpers/model.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Observable } from 'rxjs'; - -/** - * 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', -} - -export enum NbPopoverLogicalPlacement { - START = 'start', - END = 'end', -} - -/** - * 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/placement.helper.ts b/src/framework/theme/components/popover/helpers/placement.helper.ts deleted file mode 100644 index 62c02c5e45..0000000000 --- a/src/framework/theme/components/popover/helpers/placement.helper.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Injectable } from '@angular/core'; -import { NbLayoutDirectionService } from '../../../services/direction.service'; -import { NbPopoverPlacement, NbPopoverLogicalPlacement } from './model'; - -@Injectable() -export class NbPlacementHelper { - constructor(private layoutDirectionService: NbLayoutDirectionService) {} - - /* - * Maps logical position to physical according to current layout direction. - * */ - public toPhysicalPlacement( - placement: NbPopoverPlacement | NbPopoverLogicalPlacement, - ): NbPopoverPlacement { - const isLtr = this.layoutDirectionService.isLtr(); - - if (placement === NbPopoverLogicalPlacement.START) { - return isLtr ? NbPopoverPlacement.LEFT : NbPopoverPlacement.RIGHT; - } - if (placement === NbPopoverLogicalPlacement.END) { - return isLtr ? NbPopoverPlacement.RIGHT : NbPopoverPlacement.LEFT; - } - - return placement; - } -} diff --git a/src/framework/theme/components/popover/helpers/positioning.helper.ts b/src/framework/theme/components/popover/helpers/positioning.helper.ts deleted file mode 100644 index d2721c79e4..0000000000 --- a/src/framework/theme/components/popover/helpers/positioning.helper.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @license - * Copyright Akveo. All Rights Reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ -import { Injectable, Inject } from '@angular/core'; - -import { NbPopoverPlacement } from './model'; -import { NB_WINDOW } from '../../../theme.options'; - -@Injectable() -export class NbPositioningHelper { - - constructor(@Inject(NB_WINDOW) private window) { - } - - /** - * 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. - * */ - 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 += this.window.pageYOffset; - position.left += this.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 deleted file mode 100644 index b9ff31acee..0000000000 --- a/src/framework/theme/components/popover/helpers/positioning.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * @license - * Copyright Akveo. All Rights Reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ -import { async, inject, TestBed } from '@angular/core/testing'; - -import { NbPositioningHelper } from './positioning.helper'; -import { NbPopoverPlacement } from './model'; -import { NB_DOCUMENT, NB_WINDOW } from '../../../theme.options'; - -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, - }; - - let positioningHelper: NbPositioningHelper; - - beforeEach(() => { - // Configure testbed to prepare services - TestBed.configureTestingModule({ - providers: [ - { provide: NB_WINDOW, useValue: window }, - { provide: NB_DOCUMENT, useValue: document }, - NbPositioningHelper, - ], - }); - }); - - // Single async inject to save references; which are used in all tests below - beforeEach(async(inject( - [NbPositioningHelper], - (_positioningHelper) => { - positioningHelper = _positioningHelper - }, - ))); - - it('correctly locates top placement', () => { - const position = positioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.TOP); - expect(position.top).toEqual(40); - expect(position.left).toEqual(125); - }); - - it('correctly locates bottom placement', () => { - const position = positioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.BOTTOM); - expect(position.top).toEqual(210); - expect(position.left).toEqual(125); - }); - - it('correctly locates left placement', () => { - const position = positioningHelper.calcPosition(placedRect, hostRect, NbPopoverPlacement.LEFT); - expect(position.top).toEqual(125); - expect(position.left).toEqual(40); - }); - - it('correctly locates right placement', () => { - const position = positioningHelper.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 = positioningHelper.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 = positioningHelper.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 = positioningHelper.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 = positioningHelper.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 deleted file mode 100644 index 40d3c25047..0000000000 --- a/src/framework/theme/components/popover/helpers/trigger.helper.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Injectable, Inject } from '@angular/core'; -import { fromEvent as observableFromEvent, EMPTY as EMPTY$ } from 'rxjs'; -import { filter, delay, takeWhile, debounceTime, switchMap, repeat, takeUntil } from 'rxjs/operators'; - - -import { NB_DOCUMENT } from '../../../theme.options'; -import { NbPopoverMode, NbPopoverTrigger } from './model'; - -/** - * 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. - * @param document {Document} document ref. - * - * @return {NbPopoverTrigger} open and close events streams. - * */ - [NbPopoverMode.CLICK](host: HTMLElement, getContainer: Function, document: Document): NbPopoverTrigger { - return { - open: EMPTY$, - 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. - * @param document {Document} document ref. - * - * @return {NbPopoverTrigger} open and close events streams. - * */ - [NbPopoverMode.HOVER](host: HTMLElement, getContainer: Function, document: Document): 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: EMPTY$, - } - }, - - /** - * 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: EMPTY$, - } - }, -}; - -@Injectable() -export class NbTriggerHelper { - - constructor(@Inject(NB_DOCUMENT) private document) { - } - - /** - * 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. - * */ - createTrigger(host: HTMLElement, getContainer: Function, mode: NbPopoverMode): NbPopoverTrigger { - const createTrigger = NB_TRIGGERS[mode]; - return createTrigger.call(NB_TRIGGERS, host, getContainer, this.document); - } -} diff --git a/src/framework/theme/components/popover/popover.component.scss b/src/framework/theme/components/popover/popover.component.scss index 706567ea23..2bfda8882a 100644 --- a/src/framework/theme/components/popover/popover.component.scss +++ b/src/framework/theme/components/popover/popover.component.scss @@ -5,62 +5,13 @@ */ :host { - $arrow-size: 11px; - $arrow-content-size: 9px; - $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-content-size}); - border-left: $arrow-content-size solid transparent; - border-right: $arrow-content-size solid transparent; - } - } - - &.bottom .arrow { - top: -#{$arrow-size}; - left: calc(50% - #{$arrow-size}); - } - - &.left .arrow { - right: round(-$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: round(-$arrow-size - $arrow-size / 2); - top: calc(50% - #{$arrow-size / 2}); - transform: rotate(270deg); + /deep/ nb-overlay-container .primitive-overlay { + padding: 0.75rem 1rem; } } diff --git a/src/framework/theme/components/popover/popover.component.ts b/src/framework/theme/components/popover/popover.component.ts index 1183fdff0c..b3b2d6dfa2 100644 --- a/src/framework/theme/components/popover/popover.component.ts +++ b/src/framework/theme/components/popover/popover.component.ts @@ -4,19 +4,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { ChangeDetectorRef, Component, HostBinding, Input, TemplateRef, Type, ViewChild } from '@angular/core'; -import { NbPopoverPlacement } from './helpers/model'; -import { NgComponentOutlet } from '@angular/common'; +import { Component, Input } from '@angular/core'; +import { NbPositionedContainer } from '../cdk'; -/** - * 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. + * Overlay container. * Renders provided content inside. * * @styles @@ -31,88 +24,10 @@ export type NbPopoverContent = string | TemplateRef | Type; styleUrls: ['./popover.component.scss'], template: ` - - - - - - -
{{content}}
-
+ `, }) -export class NbPopoverComponent { - - /** - * Content which will be rendered. - * */ - @Input() - content: NbPopoverContent; - - /** - * Context which will be passed to rendered component instance. - * */ - @Input() - context: Object; - - /** - * Popover placement relatively host element. - * */ - @Input() - @HostBinding('class') - placement: NbPopoverPlacement = NbPopoverPlacement.TOP; - - @Input() - @HostBinding('style.top.px') - positionTop: number = 0; - - @Input() - @HostBinding('style.left.px') - positionLeft: number = 0; - - /** - * If content type is TemplateRef we're passing context as template outlet param. - * But if we have custom component content we're just assigning passed context to the component instance. - * */ - @ViewChild(NgComponentOutlet) - set componentOutlet(el) { - if (this.isComponent) { - Object.assign(el._componentRef.instance, this.context); - /** - * Change detection have to performed here, because another way applied context - * will be rendered on the next change detection loop and - * we'll have incorrect positioning. Because rendered component may change its size - * based on the context. - * */ - this.changeDetectorRef.detectChanges(); - } - } - - /** - * 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; - } - - constructor(private changeDetectorRef: ChangeDetectorRef) { - } +export class NbPopoverComponent extends NbPositionedContainer { + @Input() content: any; + @Input() context: Object; } diff --git a/src/framework/theme/components/popover/popover.directive.ts b/src/framework/theme/components/popover/popover.directive.ts index e207096104..269d7dfadc 100644 --- a/src/framework/theme/components/popover/popover.directive.ts +++ b/src/framework/theme/components/popover/popover.directive.ts @@ -4,26 +4,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { - ComponentFactoryResolver, ComponentRef, Directive, ElementRef, HostListener, Input, OnDestroy, - OnInit, PLATFORM_ID, Inject, -} from '@angular/core'; -import { isPlatformBrowser } from '@angular/common'; +import { AfterViewInit, ComponentRef, Directive, ElementRef, Inject, Input, OnDestroy } from '@angular/core'; import { takeWhile } from 'rxjs/operators'; -import { NbPositioningHelper } from './helpers/positioning.helper'; -import { NbPopoverComponent, NbPopoverContent } from './popover.component'; -import { NbThemeService } from '../../services/theme.service'; -import { NbAdjustmentHelper } from './helpers/adjustment.helper'; -import { NbTriggerHelper } from './helpers/trigger.helper'; import { - NbPopoverAdjustment, - NbPopoverMode, - NbPopoverPlacement, - NbPopoverPosition, - NbPopoverLogicalPlacement, -} from './helpers/model'; -import { NbPlacementHelper } from './helpers/placement.helper'; + NbAdjustableConnectedPositionStrategy, + NbAdjustment, + NbOverlayContent, + NbOverlayRef, + NbOverlayService, + NbPosition, + NbPositionBuilderService, + NbTrigger, + NbTriggerStrategy, + NbTriggerStrategyBuilder, + patch, + createContainer, +} from '../cdk'; +import { NB_DOCUMENT } from '../../theme.options'; +import { NbPopoverComponent } from './popover.component'; + /** * Powerful popover directive, which provides the best UX for your users. @@ -46,7 +46,7 @@ import { NbPlacementHelper } from './helpers/placement.helper'; * * ``` * - * Both custom components and templateRef popovers can receive *context* property + * Both custom components and templateRef popovers can receive *contentContext* property * that will be passed to the content props. * * Primitive types @@ -61,19 +61,19 @@ import { NbPlacementHelper } from './helpers/placement.helper'; * @stacked-example(Placements, popover/popover-placements.component) * * 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. + * and provide the best user experience. It will try to change position of the popover container. * If you wanna disable this behaviour just set it falsy value. * * ```html * * ``` * - * Also popover has some different modes which provides capability show and hide popover in different ways: + * Also popover has some different modes which provides capability show$ and hide$ popover in different ways: * * - Click mode popover shows when a user clicking on the host element and hides when the user clicks * somewhere on the document except popover. - * - Hint mode provides capability show popover when the user hovers on the host element - * and hide popover when user hovers out of the host. + * - Hint mode provides capability show$ popover when the user hovers on the host element + * and hide$ popover when user hovers out of the host. * - Hover mode works like hint mode with one exception - when the user moves mouse from host element to * the container element popover will not be hidden. * @@ -82,101 +82,66 @@ import { NbPlacementHelper } from './helpers/placement.helper'; * @additional-example(Template Ref, popover/popover-template-ref.component) * @additional-example(Custom Component, popover/popover-custom-component.component) * */ -/* -* -* 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 { +export class NbPopoverDirective implements AfterViewInit, OnDestroy { /** - * Popover content which will be rendered in NbPopoverComponent. + * Popover content which will be rendered in NbArrowedOverlayContainerComponent. * Available content: template ref, component and any primitive. * */ @Input('nbPopover') - content: NbPopoverContent; + content: NbOverlayContent; /** * Container content context. Will be applied to the rendered component. * */ @Input('nbPopoverContext') - context: Object; + context: Object = {}; /** - * Position will be calculated relatively host element based on the placement. + * Position will be calculated relatively host element based on the position. * Can be top, right, bottom, left, start or end. * */ @Input('nbPopoverPlacement') - placement: NbPopoverPlacement | NbPopoverLogicalPlacement = NbPopoverPlacement.TOP; + position: NbPosition = NbPosition.TOP; /** - * Container placement will be changes automatically based on this strategy if container can't fit view port. + * Container position 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; + adjustment: NbAdjustment = NbAdjustment.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; + mode: NbTrigger = NbTrigger.CLICK; - private containerRef: ComponentRef; + protected ref: NbOverlayRef; + protected container: ComponentRef; + protected positionStrategy: NbAdjustableConnectedPositionStrategy; + protected triggerStrategy: NbTriggerStrategy; + protected alive: boolean = true; - private get container(): NbPopoverComponent { - return this.containerRef.instance; + constructor(@Inject(NB_DOCUMENT) protected document, + private hostRef: ElementRef, + private positionBuilder: NbPositionBuilderService, + private overlay: NbOverlayService) { } - private get containerElement(): HTMLElement { - return this.containerRef.location.nativeElement; - } + ngAfterViewInit() { + this.positionStrategy = this.createPositionStrategy(); + this.ref = this.overlay.create({ + positionStrategy: this.positionStrategy, + scrollStrategy: this.overlay.scrollStrategies.reposition(), + }); + this.triggerStrategy = this.createTriggerStrategy(); - private get hostElement(): HTMLElement { - return this.hostRef.nativeElement; - } - - constructor( - private hostRef: ElementRef, - private themeService: NbThemeService, - private componentFactoryResolver: ComponentFactoryResolver, - private positioningHelper: NbPositioningHelper, - private adjustmentHelper: NbAdjustmentHelper, - private triggerHelper: NbTriggerHelper, - @Inject(PLATFORM_ID) private platformId, - private placementHelper: NbPlacementHelper, - ) {} - - ngOnInit() { - if (isPlatformBrowser(this.platformId)) { - this.registerTriggers(); - } + this.subscribeOnTriggers(); + this.subscribeOnPositionChange(); } ngOnDestroy() { @@ -184,177 +149,51 @@ export class NbPopoverDirective implements OnInit, OnDestroy { this.hide(); } - /** - * Show popover. - * */ show() { - if (this.isHidden) { - this.renderPopover(); - } + this.container = createContainer(this.ref, NbPopoverComponent, { + position: this.position, + content: this.content, + context: this.context, + }); } - /** - * Hide popover. - * */ hide() { - if (this.isShown) { - this.destroyPopover(); - } + this.ref.detach(); + this.container = null; } - /** - * Toggle popover state. - * */ toggle() { - if (this.isShown) { + if (this.ref && this.ref.hasAttached()) { 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(); - } + protected createPositionStrategy(): NbAdjustableConnectedPositionStrategy { + return this.positionBuilder + .connectedTo(this.hostRef) + .position(this.position) + .adjustment(this.adjustment); } - /* - * Subscribe to the popover triggers created from the {@link NbPopoverDirective#mode}. - * see {@link NbTriggerHelper} - * */ - private registerTriggers() { - const { open, close, toggle } = this.triggerHelper - .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()); + protected createTriggerStrategy(): NbTriggerStrategy { + return new NbTriggerStrategyBuilder() + .document(this.document) + .trigger(this.mode) + .host(this.hostRef.nativeElement) + .container(() => this.container) + .build(); } - /* - * 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() { - const factory = this.componentFactoryResolver.resolveComponentFactory(NbPopoverComponent); - this.themeService.appendToLayoutTop(factory) + protected subscribeOnPositionChange() { + this.positionStrategy.positionChange .pipe(takeWhile(() => this.alive)) - .subscribe((containerRef: ComponentRef) => { - this.containerRef = containerRef; - this.patchPopover(this.content, this.context); - /* - * 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.markForCheck(); - 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 and context. - * */ - private patchPopover(content: NbPopoverContent, context: Object) { - this.container.content = content; - this.container.context = context; - } - - /* - * Set container placement. - * */ - private patchPopoverPlacement(placement: NbPopoverPlacement) { - this.container.placement = placement; - } - - /* - * Set container position. - * */ - private patchPopoverPosition({ top, 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); + .subscribe((position: NbPosition) => patch(this.container, { 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 { - const placement = this.placementHelper.toPhysicalPlacement(this.placement); - return this.adjustmentHelper.adjust(placed, host, placement, this.adjustment); - } - - /* - * Calculate position. - * see {@link NbPositioningHelper} - * */ - private calcPosition(placed: ClientRect, host: ClientRect): NbPopoverPosition { - const placement = this.placementHelper.toPhysicalPlacement(this.placement); - return { - position: this.positioningHelper.calcPosition(placed, host, placement), - placement, - }; + protected subscribeOnTriggers() { + this.triggerStrategy.show$.pipe(takeWhile(() => this.alive)).subscribe(() => this.show()); + this.triggerStrategy.hide$.pipe(takeWhile(() => this.alive)).subscribe(() => this.hide()); } } diff --git a/src/framework/theme/components/popover/popover.module.ts b/src/framework/theme/components/popover/popover.module.ts index 59d0556a63..b155ac7bb6 100644 --- a/src/framework/theme/components/popover/popover.module.ts +++ b/src/framework/theme/components/popover/popover.module.ts @@ -5,20 +5,17 @@ */ import { NgModule } from '@angular/core'; -import { NbPopoverComponent } from './popover.component'; -import { NbSharedModule } from '../shared/shared.module'; + +import { NbOverlayModule } from '../cdk'; import { NbPopoverDirective } from './popover.directive'; -import { NbAdjustmentHelper } from './helpers/adjustment.helper'; -import { NbPositioningHelper } from './helpers/positioning.helper'; -import { NbTriggerHelper } from './helpers/trigger.helper'; -import { NbPlacementHelper } from './helpers/placement.helper'; +import { NbPopoverComponent } from './popover.component'; + @NgModule({ - imports: [NbSharedModule], - declarations: [NbPopoverComponent, NbPopoverDirective], + imports: [NbOverlayModule], + declarations: [NbPopoverDirective, NbPopoverComponent], exports: [NbPopoverDirective], entryComponents: [NbPopoverComponent], - providers: [NbAdjustmentHelper, NbPositioningHelper, NbTriggerHelper, NbPlacementHelper], }) export class NbPopoverModule { } diff --git a/src/framework/theme/components/search/search.component.ts b/src/framework/theme/components/search/search.component.ts index b0486f7385..b13749d0a8 100644 --- a/src/framework/theme/components/search/search.component.ts +++ b/src/framework/theme/components/search/search.component.ts @@ -8,8 +8,6 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, - ComponentFactoryResolver, - ComponentRef, ElementRef, EventEmitter, HostBinding, @@ -18,15 +16,18 @@ import { OnInit, Output, ViewChild, - ViewContainerRef, + ChangeDetectorRef, + OnChanges, + SimpleChanges, } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; -import { BehaviorSubject, of as observableOf, combineLatest } from 'rxjs'; +import { of as observableOf } from 'rxjs'; import { filter, delay, takeWhile } from 'rxjs/operators'; import { NbSearchService } from './search.service'; import { NbThemeService } from '../../services/theme.service'; +import { NbOverlayService, NbOverlayRef, NbPortalDirective } from '../cdk'; /** * search-field-component is used under the hood by nb-search component @@ -45,8 +46,8 @@ import { NbThemeService } from '../../services/theme.service'; 'styles/search.component.modal-half.scss', ], template: ` - `, }) -export class NbSearchFieldComponent { +export class NbSearchFieldComponent implements OnChanges, AfterViewInit { static readonly TYPE_MODAL_ZOOMIN = 'modal-zoomin'; static readonly TYPE_ROTATE_LAYOUT = 'rotate-layout'; @@ -75,61 +76,71 @@ export class NbSearchFieldComponent { static readonly TYPE_MODAL_DROP = 'modal-drop'; static readonly TYPE_MODAL_HALF = 'modal-half'; - @Input() searchType: string; + @Input() type: string; @Input() placeholder: string; @Input() hint: string; + @Input() show = false; - @Output() searchClose = new EventEmitter(); + @Output() close = new EventEmitter(); @Output() search = new EventEmitter(); - @Output() tabOut = new EventEmitter(); + @ViewChild('searchInput') inputElement: ElementRef; - @ViewChild('searchInput') inputElement: ElementRef; - - @Input() @HostBinding('class.show') showSearch: boolean = false; + @HostBinding('class.show') + get showClass() { + return this.show; + } @HostBinding('class.modal-zoomin') get modalZoomin() { - return this.searchType === NbSearchFieldComponent.TYPE_MODAL_ZOOMIN; + return this.type === NbSearchFieldComponent.TYPE_MODAL_ZOOMIN; } @HostBinding('class.rotate-layout') get rotateLayout() { - return this.searchType === NbSearchFieldComponent.TYPE_ROTATE_LAYOUT; + return this.type === NbSearchFieldComponent.TYPE_ROTATE_LAYOUT; } @HostBinding('class.modal-move') get modalMove() { - return this.searchType === NbSearchFieldComponent.TYPE_MODAL_MOVE; + return this.type === NbSearchFieldComponent.TYPE_MODAL_MOVE; } @HostBinding('class.curtain') get curtain() { - return this.searchType === NbSearchFieldComponent.TYPE_CURTAIN; + return this.type === NbSearchFieldComponent.TYPE_CURTAIN; } @HostBinding('class.column-curtain') get columnCurtain() { - return this.searchType === NbSearchFieldComponent.TYPE_COLUMN_CURTAIN; + return this.type === NbSearchFieldComponent.TYPE_COLUMN_CURTAIN; } @HostBinding('class.modal-drop') get modalDrop() { - return this.searchType === NbSearchFieldComponent.TYPE_MODAL_DROP; + return this.type === NbSearchFieldComponent.TYPE_MODAL_DROP; } @HostBinding('class.modal-half') get modalHalf() { - return this.searchType === NbSearchFieldComponent.TYPE_MODAL_HALF; + return this.type === NbSearchFieldComponent.TYPE_MODAL_HALF; } - @Input() - set type(val: any) { - this.searchType = val; + ngOnChanges({ show }: SimpleChanges) { + const becameHidden = !show.isFirstChange() && show.currentValue === false; + if (becameHidden && this.inputElement) { + this.inputElement.nativeElement.value = ''; + } + + this.focusInput(); } - closeSearch() { - this.searchClose.emit(true); + ngAfterViewInit() { + this.focusInput(); + } + + emitClose() { + this.close.emit(); } submitSearch(term) { @@ -137,6 +148,12 @@ export class NbSearchFieldComponent { this.search.emit(term); } } + + focusInput() { + if (this.show && this.inputElement) { + this.inputElement.nativeElement.focus(); + } + } } /** @@ -173,15 +190,25 @@ export class NbSearchFieldComponent { changeDetection: ChangeDetectionStrategy.OnPush, styleUrls: ['styles/search.component.scss'], template: ` - - + + `, }) -export class NbSearchComponent implements OnInit, AfterViewInit, OnDestroy { +export class NbSearchComponent implements OnInit, OnDestroy { private alive = true; + private overlayRef: NbOverlayRef; + showSearchField = false; /** * Tags a search with some ID, can be later used in the search service @@ -204,28 +231,23 @@ export class NbSearchComponent implements OnInit, AfterViewInit, OnDestroy { */ @Input() hint: string = 'Hit enter to search'; - @HostBinding('class.show') showSearch: boolean = false; - - @ViewChild('attachedSearchContainer', {read: ViewContainerRef}) attachedSearchContainer: ViewContainerRef; - - private searchFieldComponentRef$ = new BehaviorSubject>(null); - private searchType: string = 'rotate-layout'; - - constructor(private searchService: NbSearchService, - private componentFactoryResolver: ComponentFactoryResolver, - private themeService: NbThemeService, - private router: Router) { - } - /** * Search design type, available types are * modal-zoomin, rotate-layout, modal-move, curtain, column-curtain, modal-drop, modal-half * @type {string} */ - @Input() - set type(val: any) { - this.searchType = val; - } + @Input() type: string; + + @ViewChild(NbPortalDirective) searchFieldPortal: NbPortalDirective; + @ViewChild('searchButton') searchButton: ElementRef; + + constructor( + private searchService: NbSearchService, + private themeService: NbThemeService, + private router: Router, + private overlayService: NbOverlayService, + private changeDetector: ChangeDetectorRef, + ) {} ngOnInit() { this.router.events @@ -233,90 +255,62 @@ export class NbSearchComponent implements OnInit, AfterViewInit, OnDestroy { takeWhile(() => this.alive), filter(event => event instanceof NavigationEnd), ) - .subscribe(event => this.searchService.deactivateSearch(this.searchType, this.tag)); + .subscribe(() => this.hideSearch()); - combineLatest([ - this.searchFieldComponentRef$, - this.searchService.onSearchActivate(), - ]) + this.searchService.onSearchActivate() .pipe( takeWhile(() => this.alive), - filter(([componentRef, data]: [ComponentRef, any]) => componentRef != null), - filter(([componentRef, data]: [ComponentRef, any]) => !this.tag || data.tag === this.tag), + filter(data => !this.tag || data.tag === this.tag), ) - .subscribe(([componentRef, data]: [ComponentRef, any]) => { - this.showSearch = true; - - this.themeService.appendLayoutClass(this.searchType); - observableOf(null).pipe(delay(0)).subscribe(() => { - this.themeService.appendLayoutClass('with-search'); - }); - componentRef.instance.showSearch = true; - componentRef.instance.inputElement.nativeElement.focus(); - componentRef.changeDetectorRef.detectChanges(); - }); - - combineLatest([ - this.searchFieldComponentRef$, - this.searchService.onSearchDeactivate(), - ]) + .subscribe(() => this.openSearch()); + + this.searchService.onSearchDeactivate() .pipe( takeWhile(() => this.alive), - filter(([componentRef, data]: [ComponentRef, any]) => componentRef != null), - filter(([componentRef, data]: [ComponentRef, any]) => !this.tag || data.tag === this.tag), + filter(data => !this.tag || data.tag === this.tag), ) - .subscribe(([componentRef, data]: [ComponentRef, any]) => { - this.showSearch = false; - - componentRef.instance.showSearch = false; - componentRef.instance.inputElement.nativeElement.value = ''; - componentRef.instance.inputElement.nativeElement.blur(); - componentRef.changeDetectorRef.detectChanges(); - - this.themeService.removeLayoutClass('with-search'); - observableOf(null).pipe(delay(500)).subscribe(() => { - this.themeService.removeLayoutClass(this.searchType); - }); - }); + .subscribe(() => this.hideSearch()); } - ngAfterViewInit() { - const factory = this.componentFactoryResolver.resolveComponentFactory(NbSearchFieldComponent); - this.themeService.appendToLayoutTop(factory) - .subscribe((componentRef: ComponentRef) => { - this.connectToSearchField(componentRef); - }); + ngOnDestroy() { + if (this.overlayRef && this.overlayRef.hasAttached()) { + this.removeLayoutClasses(); + this.overlayRef.detach(); + } + + this.alive = false; } openSearch() { - this.searchService.activateSearch(this.searchType, this.tag); - } + if (!this.overlayRef) { + this.overlayRef = this.overlayService.create(); + this.overlayRef.attach(this.searchFieldPortal); + } - connectToSearchField(componentRef) { - componentRef.instance.searchType = this.searchType; - componentRef.instance.placeholder = this.placeholder; - componentRef.instance.hint = this.hint; - componentRef.instance.searchClose.subscribe(() => { - this.searchService.deactivateSearch(this.searchType, this.tag); + this.themeService.appendLayoutClass(this.type); + observableOf(null).pipe(delay(0)).subscribe(() => { + this.themeService.appendLayoutClass('with-search'); + this.showSearchField = true; + this.changeDetector.detectChanges(); }); - componentRef.instance.search.subscribe(term => { - this.searchService.submitSearch(term, this.tag); - this.searchService.deactivateSearch(this.searchType, this.tag); - }); - componentRef.instance.tabOut - .subscribe(() => this.showSearch && componentRef.instance.inputElement.nativeElement.focus()); - - componentRef.changeDetectorRef.detectChanges(); + } - this.searchFieldComponentRef$.next(componentRef) + hideSearch() { + this.removeLayoutClasses(); + this.showSearchField = false; + this.changeDetector.detectChanges(); + this.searchButton.nativeElement.focus(); } - ngOnDestroy() { - this.alive = false; + search(term) { + this.searchService.submitSearch(term, this.tag); + this.hideSearch(); + } - const componentRef = this.searchFieldComponentRef$.getValue(); - if (componentRef) { - componentRef.destroy(); - } + private removeLayoutClasses() { + this.themeService.removeLayoutClass('with-search'); + observableOf(null).pipe(delay(500)).subscribe(() => { + this.themeService.removeLayoutClass(this.type); + }); } } diff --git a/src/framework/theme/components/search/search.module.ts b/src/framework/theme/components/search/search.module.ts index 917e803c3e..96b2e545de 100644 --- a/src/framework/theme/components/search/search.module.ts +++ b/src/framework/theme/components/search/search.module.ts @@ -6,6 +6,8 @@ import { NgModule } from '@angular/core'; import { NbSharedModule } from '../shared/shared.module'; +import { NbOverlayModule } from '../cdk/overlay/overlay.module'; + import { NbSearchComponent, NbSearchFieldComponent } from './search.component'; import { NbSearchService } from './search.service'; @@ -13,6 +15,7 @@ import { NbSearchService } from './search.service'; @NgModule({ imports: [ NbSharedModule, + NbOverlayModule, ], declarations: [ NbSearchComponent, @@ -29,4 +32,5 @@ import { NbSearchService } from './search.service'; NbSearchFieldComponent, ], }) -export class NbSearchModule { } +export class NbSearchModule { +} diff --git a/src/framework/theme/components/search/styles/search.component.curtain.scss b/src/framework/theme/components/search/styles/search.component.curtain.scss index 63e84fd849..8f7c285779 100644 --- a/src/framework/theme/components/search/styles/search.component.curtain.scss +++ b/src/framework/theme/components/search/styles/search.component.curtain.scss @@ -91,3 +91,8 @@ } } } + +/deep/ nb-layout.curtain .scrollable-container { + position: relative; + z-index: 0; +} diff --git a/src/framework/theme/components/search/styles/search.component.layout-rotate.scss b/src/framework/theme/components/search/styles/search.component.layout-rotate.scss index c50a9e3612..f8dc465582 100644 --- a/src/framework/theme/components/search/styles/search.component.layout-rotate.scss +++ b/src/framework/theme/components/search/styles/search.component.layout-rotate.scss @@ -11,6 +11,13 @@ overflow: hidden; width: 100%; + .scrollable-container { + position: relative; + z-index: 10001; + + transition: transform 0.5s cubic-bezier(0.2, 1, 0.3, 1); + } + &.with-search .scrollable-container { transition: transform 0.5s cubic-bezier(0.2, 1, 0.3, 1); transform-origin: 50vw 50vh; @@ -23,7 +30,7 @@ position: absolute; display: block; - width: 100%; + width: 100vw; height: 100vh; pointer-events: none; opacity: 0; diff --git a/src/framework/theme/components/search/styles/search.component.scss b/src/framework/theme/components/search/styles/search.component.scss index 1874986ddf..04c50fced8 100644 --- a/src/framework/theme/components/search/styles/search.component.scss +++ b/src/framework/theme/components/search/styles/search.component.scss @@ -22,3 +22,8 @@ } } +/deep/ nb-layout.with-search .scrollable-container { + position: relative; + z-index: 0; +} + diff --git a/src/framework/theme/services/theme.service.ts b/src/framework/theme/services/theme.service.ts index 47186d991b..10fcff1be5 100644 --- a/src/framework/theme/services/theme.service.ts +++ b/src/framework/theme/services/theme.service.ts @@ -4,9 +4,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { ComponentFactory, ComponentFactoryResolver, Inject, Injectable, Type } from '@angular/core'; +import { Inject, Injectable } from '@angular/core'; -import { Observable, ReplaySubject, Subject, BehaviorSubject } from 'rxjs'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; import { map, filter, pairwise, distinctUntilChanged, startWith, share } from 'rxjs/operators'; import { NB_THEME_OPTIONS } from '../theme.options'; @@ -23,18 +23,13 @@ export class NbThemeService { // TODO: behavioral subject here? currentTheme: string; private themeChanges$ = new ReplaySubject(1); - private appendToLayoutTop$ = new ReplaySubject(); - private createLayoutTop$ = new Subject(); private appendLayoutClass$ = new Subject(); private removeLayoutClass$ = new Subject(); private changeWindowWidth$ = new ReplaySubject(2); - constructor( - @Inject(NB_THEME_OPTIONS) protected options: any, - private breakpointService: NbMediaBreakpointsService, - private jsThemesRegistry: NbJSThemesRegistry, - private componentFactoryResolver: ComponentFactoryResolver, - ) { + constructor(@Inject(NB_THEME_OPTIONS) protected options: any, + private breakpointService: NbMediaBreakpointsService, + private jsThemesRegistry: NbJSThemesRegistry) { if (options && options.name) { this.changeTheme(options.name); } @@ -53,25 +48,6 @@ export class NbThemeService { this.changeWindowWidth$.next(width); } - /** - * Append a component to top of the layout - * (useful for showing modal that should be placed heigher in the document tree) - * - * @param {Type | ComponentFactory} entity - * @returns {Observable} - */ - appendToLayoutTop(entity: Type | ComponentFactory): Observable { - let factory = entity; - - if (entity instanceof Type) { - factory = this.componentFactoryResolver.resolveComponentFactory(entity); - } - - const subject = new ReplaySubject(1); - this.appendToLayoutTop$.next({ factory, listener: subject }); - return subject.asObservable(); - } - /** * Returns a theme object with variables (color/paddings/etc) on a theme change. * Once subscribed - returns current theme. @@ -86,17 +62,6 @@ export class NbThemeService { ); } - /** - * Clears layout top - * @returns {Observable} - */ - clearLayoutTop(): Observable { - const observable = new BehaviorSubject(null); - this.createLayoutTop$.next({ listener: observable }); - this.appendToLayoutTop$ = new ReplaySubject(); - return observable.asObservable(); - } - /** * Triggers media query breakpoint change * Returns a pair where the first item is previous media breakpoint and the second item is current breakpoit. @@ -132,14 +97,6 @@ export class NbThemeService { return this.themeChanges$.pipe(share()); } - onAppendToTop(): Observable { - return this.appendToLayoutTop$.pipe(share()); - } - - onClearLayoutTop(): Observable { - return this.createLayoutTop$.pipe(share()); - } - /** * Append a class to nb-layout * @param {string} className diff --git a/src/framework/theme/styles/global/_components.scss b/src/framework/theme/styles/global/_components.scss index 520045a18f..a0c0597337 100644 --- a/src/framework/theme/styles/global/_components.scss +++ b/src/framework/theme/styles/global/_components.scss @@ -31,6 +31,8 @@ @import '../../components/list/list.component.theme'; @import '../../components/input/input.directive.theme'; @import '../../components/cdk/overlay/overlay.theme'; +@import '../../components/popover/popover.component.theme'; +@import '../../components/context-menu/context-menu.component.theme'; @mixin nb-theme-components() { @@ -61,4 +63,6 @@ @include nb-list-theme(); @include nb-input-theme(); @include nb-overlay-theme(); + @include nb-popover-theme(); + @include nb-context-menu-theme(); } diff --git a/src/framework/theme/styles/themes/_cosmic.scss b/src/framework/theme/styles/themes/_cosmic.scss index 97a10cdde2..7407bb66e9 100644 --- a/src/framework/theme/styles/themes/_cosmic.scss +++ b/src/framework/theme/styles/themes/_cosmic.scss @@ -66,6 +66,7 @@ $theme: ( popover-shadow: shadow, context-menu-active-bg: color-primary, + context-menu-border: color-primary, footer-height: header-height, diff --git a/src/framework/theme/styles/themes/_default.scss b/src/framework/theme/styles/themes/_default.scss index dda0ad46a1..609c177320 100644 --- a/src/framework/theme/styles/themes/_default.scss +++ b/src/framework/theme/styles/themes/_default.scss @@ -241,11 +241,18 @@ $theme: ( popover-fg: color-fg-heading, popover-bg: color-bg, popover-border: color-success, + popover-border-radius: radius, popover-shadow: none, + popover-arrow-size: 11px, context-menu-fg: color-fg-heading, + context-menu-bg: color-bg, context-menu-active-fg: color-white, context-menu-active-bg: color-success, + context-menu-border: color-success, + context-menu-border-radius: radius, + context-menu-shadow: none, + context-menu-arrow-size: 11px, actions-font-size: font-size, actions-font-family: font-secondary, diff --git a/src/playground/layout/theme-dynamic-test.component.ts b/src/playground/layout/theme-dynamic-test.component.ts deleted file mode 100644 index 48604c27d7..0000000000 --- a/src/playground/layout/theme-dynamic-test.component.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @license - * Copyright Akveo. All Rights Reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - */ - -import { Component, ComponentFactoryResolver } from '@angular/core'; - -import { NbThemeService } from '@nebular/theme'; -import { NbDynamicToAddComponent } from '../shared/dynamic.component'; - -@Component({ - selector: 'nb-dynamic-test', - template: ` - - - Akveo - - - - - - - Sidebar content - - - - - Hello - - Some Test content - - - - - - - © Akveo 2017 - - - `, -}) -export class NbThemeDynamicTestComponent { - constructor(private themeService: NbThemeService, private componentFactoryResolver: ComponentFactoryResolver) { - } - - addDynamicComponent() { - this.themeService.appendToLayoutTop(NbDynamicToAddComponent).subscribe(cRef => console.info(cRef)); - } - - addDynamicByFactory() { - const factory = this.componentFactoryResolver.resolveComponentFactory(NbDynamicToAddComponent); - this.themeService.appendToLayoutTop(factory).subscribe(cRef => console.info(cRef)); - } - - clearDynamicComponents() { - this.themeService.clearLayoutTop().subscribe(res => console.info(res)); - } -} diff --git a/src/playground/playground-routing.module.ts b/src/playground/playground-routing.module.ts index 7fe4e8a252..2cccef9e0d 100644 --- a/src/playground/playground-routing.module.ts +++ b/src/playground/playground-routing.module.ts @@ -66,7 +66,6 @@ import { NbContextMenuTestComponent } from './context-menu/context-menu-test.com import { NbLayoutHeaderTestComponent } from './layout/layout-header-test.component'; import { NbLayoutFooterTestComponent } from './layout/layout-footer-test.component'; import { NbThemeChangeTestComponent } from './layout/theme-change-test.component'; -import { NbThemeDynamicTestComponent } from './layout/theme-dynamic-test.component'; import { NbThemeBreakpointTestComponent } from './layout/theme-breakpoint-test.component'; import { NbLayoutTestComponent } from './layout/layout-test.component'; import { @@ -763,10 +762,6 @@ export const routes: Routes = [ path: 'theme-change-test.component', component: NbThemeChangeTestComponent, }, - { - path: 'theme-dynamic-test.component', - component: NbThemeDynamicTestComponent, - }, { path: 'theme-breakpoint-test.component', component: NbThemeBreakpointTestComponent, diff --git a/src/playground/playground.module.ts b/src/playground/playground.module.ts index 7d472e7699..ab2e5731b7 100644 --- a/src/playground/playground.module.ts +++ b/src/playground/playground.module.ts @@ -126,7 +126,6 @@ import { NbSidebarTwoTestComponent } from './sidebar/sidebar-two-test.component' import { NbSidebarThreeTestComponent } from './sidebar/sidebar-three-test.component'; import { NbTabsetTestComponent } from './tabset/tabset-test.component'; import { NbUserTestComponent } from './user/user-test.component'; -import { NbThemeDynamicTestComponent } from './layout/theme-dynamic-test.component'; import { NbBootstrapTestComponent } from './bootstrap/bootstrap-test.component'; import { NbStepperShowcaseComponent } from './stepper/stepper-showcase.component'; import { NbStepperValidationComponent } from './stepper/stepper-validation.component'; @@ -262,7 +261,6 @@ export const NB_EXAMPLE_COMPONENTS = [ NbLayoutTestComponent, NbLayoutHeaderTestComponent, NbLayoutFooterTestComponent, - NbThemeDynamicTestComponent, NbThemeChangeTestComponent, NbThemeBreakpointTestComponent, NbSidebarTestComponent, diff --git a/src/playground/popover/popover-custom-component.component.ts b/src/playground/popover/popover-custom-component.component.ts index fc1100e02b..2c55f604ac 100644 --- a/src/playground/popover/popover-custom-component.component.ts +++ b/src/playground/popover/popover-custom-component.component.ts @@ -10,6 +10,11 @@ import { NbDynamicToAddComponent } from '../shared/dynamic.component'; @Component({ selector: 'nb-popover-custom-component', templateUrl: './popover-custom-component.component.html', + styles: [` + nb-layout-column { + height: 50vw; + } + `], }) export class NbPopoverCustomComponentComponent { diff --git a/src/playground/popover/popover-placements.component.ts b/src/playground/popover/popover-placements.component.ts index dccbbe55bc..88c4d8b9cd 100644 --- a/src/playground/popover/popover-placements.component.ts +++ b/src/playground/popover/popover-placements.component.ts @@ -12,9 +12,14 @@ import { Component } from '@angular/core'; templateUrl: './popover-placements.component.html', styles: [` :host { - margin: 2rem 0; + margin: 4rem 0; display: block; } + + nb-layout-column { + height: 50vw; + } + button { margin: 1rem; } diff --git a/src/playground/popover/popover-showcase.component.ts b/src/playground/popover/popover-showcase.component.ts index 07b72aed85..ff5fb7fae7 100644 --- a/src/playground/popover/popover-showcase.component.ts +++ b/src/playground/popover/popover-showcase.component.ts @@ -10,6 +10,12 @@ import { Component } from '@angular/core'; @Component({ selector: 'nb-popover-showcase', templateUrl: './popover-showcase.component.html', + styles: [` + :host { + display: block; + margin: 5rem; + } + `], }) export class NbPopoverShowcaseComponent { }