From 6dbce1d54a5d1f71ddc9050074e400ba5c68d72f Mon Sep 17 00:00:00 2001 From: Sergey Andrievskiy Date: Mon, 7 Oct 2019 14:28:57 +0300 Subject: [PATCH] feat(popover, tooltip): add shown state api (#1998) --- .../dynamic/dynamic-overlay-handler.spec.ts | 21 ++++- .../dynamic/dynamic-overlay-handler.ts | 21 +++++ .../overlay/dynamic/dynamic-overlay.spec.ts | 15 ++-- .../cdk/overlay/dynamic/dynamic-overlay.ts | 40 +++++++-- .../components/popover/popover.directive.ts | 47 +++++++++-- .../theme/components/popover/popover.spec.ts | 82 ++++++++++++++++++- .../components/tooltip/tooltip.directive.ts | 51 ++++++++++-- .../theme/components/tooltip/tooltip.spec.ts | 76 ++++++++++++++++- 8 files changed, 323 insertions(+), 30 deletions(-) diff --git a/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay-handler.spec.ts b/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay-handler.spec.ts index b0cf01757a..03651eda82 100644 --- a/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay-handler.spec.ts +++ b/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay-handler.spec.ts @@ -22,6 +22,7 @@ import { NbDynamicOverlay } from './dynamic-overlay'; import { NbOverlayContent } from '../overlay-service'; import { NbDynamicOverlayChange, NbDynamicOverlayHandler } from './dynamic-overlay-handler'; import { NbTrigger, NbTriggerStrategy, NbTriggerStrategyBuilderService } from '../overlay-trigger'; +import { NbOverlayConfig } from '@nebular/theme/components/cdk/overlay/mapping'; @Component({ template: '' }) export class NbDynamicOverlayMockComponent implements NbRenderableContainer { @@ -43,18 +44,21 @@ export class NbMockDynamicOverlay { _context: Object = {}; _content: NbOverlayContent; _positionStrategy: NbAdjustableConnectedPositionStrategy; + _overlayConfig: NbOverlayConfig; constructor() {} create(componentType: Type, content: NbOverlayContent, context: Object, - positionStrategy: NbAdjustableConnectedPositionStrategy) { + positionStrategy: NbAdjustableConnectedPositionStrategy, + overlayConfig: NbOverlayConfig) { this.setContext(context); this.setContent(content); this.setComponent(componentType); this.setPositionStrategy(positionStrategy); + this.setOverlayConfig(overlayConfig); return this; } @@ -71,6 +75,10 @@ export class NbMockDynamicOverlay { this._componentType = componentType; } + setOverlayConfig(overlayConfig: NbOverlayConfig) { + this._overlayConfig = overlayConfig; + } + setContentAndContext(content: NbOverlayContent, context: Object) { this._content = content; this._context = context; @@ -516,4 +524,15 @@ describe('dynamic-overlay-handler', () => { expect(positionBuilder._position).toBe(NbPosition.LEFT); expect(positionBuilder._adjustment).toBe(NbAdjustment.HORIZONTAL); }); + + it('should set and update overlay config', () => { + let overlayConfig: NbOverlayConfig = { panelClass: 'custom-class' }; + + let dynamic = configure().overlayConfig(overlayConfig).build(); + expect(dynamic._overlayConfig).toEqual(jasmine.objectContaining(overlayConfig)); + + overlayConfig = { panelClass: 'other-custom-class' }; + dynamic = configure().overlayConfig(overlayConfig).rebuild(); + expect(dynamic._overlayConfig).toEqual(jasmine.objectContaining(overlayConfig)); + }); }); diff --git a/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay-handler.ts b/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay-handler.ts index 86a4638777..75386a88db 100644 --- a/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay-handler.ts +++ b/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay-handler.ts @@ -10,6 +10,7 @@ import { import { NbRenderableContainer } from '../overlay-container'; import { NbOverlayContent } from '../overlay-service'; import { NbDynamicOverlay } from './dynamic-overlay'; +import { NbOverlayConfig } from '../mapping'; export class NbDynamicOverlayChange extends SimpleChange { @@ -33,6 +34,7 @@ export class NbDynamicOverlayHandler { protected _position: NbPosition = NbPosition.TOP; protected _adjustment: NbAdjustment = NbAdjustment.NOOP; protected _offset: number = 15; + protected _overlayConfig: NbOverlayConfig = {}; protected dynamicOverlay: NbDynamicOverlay; protected triggerStrategy: NbTriggerStrategy; @@ -94,6 +96,12 @@ export class NbDynamicOverlayHandler { return this; } + overlayConfig(overlayConfig: NbOverlayConfig) { + this.changes.overlayConfig = new NbDynamicOverlayChange(this._overlayConfig, overlayConfig); + this._overlayConfig = overlayConfig; + return this; + } + build() { if (!this._componentType || !this._host) { throw Error(`NbDynamicOverlayHandler: at least 'componentType' and 'host' should be @@ -104,6 +112,7 @@ export class NbDynamicOverlayHandler { this._content, this._context, this.createPositionStrategy(), + this._overlayConfig, ); this.connect(); @@ -139,6 +148,10 @@ export class NbDynamicOverlayHandler { this.dynamicOverlay.setComponent(this._componentType); } + if (this.isOverlayConfigUpdateRequired()) { + this.dynamicOverlay.setOverlayConfig(this._overlayConfig); + } + this.clearChanges(); return this.dynamicOverlay; } @@ -203,6 +216,10 @@ export class NbDynamicOverlayHandler { return this.isComponentTypeUpdated(); } + private isOverlayConfigUpdateRequired(): boolean { + return this.isOverlayConfigUpdated(); + } + protected isComponentTypeUpdated(): boolean { return this.changes.componentType && this.changes.componentType.isChanged(); } @@ -235,6 +252,10 @@ export class NbDynamicOverlayHandler { return this.changes.offset && this.changes.offset.isChanged(); } + protected isOverlayConfigUpdated(): boolean { + return this.changes.overlayConfig && this.changes.overlayConfig.isChanged(); + } + protected clearChanges() { this.changes = {}; } diff --git a/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay.spec.ts b/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay.spec.ts index b0058a5889..cb34f682d2 100644 --- a/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay.spec.ts +++ b/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay.spec.ts @@ -267,6 +267,16 @@ describe('dynamic-overlay', () => { expect(instance.content).toBe(newContent); }); + it('should set overlay config', () => { + const overlayConfig: NbOverlayConfig = { panelClass: 'additional-overlay-class' }; + const createOverlaySpy = spyOn(overlayService, 'create').and.callThrough(); + + dynamicOverlay.setOverlayConfig(overlayConfig); + dynamicOverlay.show(); + + expect(createOverlaySpy).toHaveBeenCalledWith(jasmine.objectContaining(overlayConfig)); + }); + it('should return container', () => { dynamicOverlay.show(); expect(dynamicOverlay.getContainer()).toBe(container as any); @@ -319,14 +329,12 @@ describe('dynamic-overlay', () => { }); it('should set component', () => { - const detachSpy = spyOn(ref, 'detach').and.callThrough(); const disposeSpy = spyOn(ref, 'dispose').and.callThrough(); const attachSpy = spyOn(ref, 'attach').and.callThrough(); const hasAttacheSpy = spyOn(ref, 'hasAttached'); dynamicOverlay.setComponent(NbDynamicOverlayMock2Component); - expect(detachSpy).toHaveBeenCalledTimes(0); expect(disposeSpy).toHaveBeenCalledTimes(0); expect(attachSpy).toHaveBeenCalledTimes(0); @@ -334,14 +342,12 @@ describe('dynamic-overlay', () => { hasAttacheSpy.and.returnValue(true); expect(ref.portal.component).toBe(NbDynamicOverlayMock2Component); - expect(detachSpy).toHaveBeenCalledTimes(0); expect(disposeSpy).toHaveBeenCalledTimes(0); expect(attachSpy).toHaveBeenCalledTimes(1); dynamicOverlay.setComponent(NbDynamicOverlayMockComponent); expect(ref.portal.component).toBe(NbDynamicOverlayMockComponent); - expect(detachSpy).toHaveBeenCalledTimes(1); expect(disposeSpy).toHaveBeenCalledTimes(1); expect(attachSpy).toHaveBeenCalledTimes(2); @@ -350,7 +356,6 @@ describe('dynamic-overlay', () => { dynamicOverlay.setComponent(NbDynamicOverlayMock2Component); - expect(detachSpy).toHaveBeenCalledTimes(3); expect(disposeSpy).toHaveBeenCalledTimes(2); expect(attachSpy).toHaveBeenCalledTimes(2); }); diff --git a/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay.ts b/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay.ts index 0b9ed44462..ea6063f80c 100644 --- a/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay.ts +++ b/src/framework/theme/components/cdk/overlay/dynamic/dynamic-overlay.ts @@ -1,6 +1,6 @@ import { ComponentFactoryResolver, ComponentRef, Injectable, NgZone, Type } from '@angular/core'; -import { filter, takeUntil, takeWhile } from 'rxjs/operators'; -import { Subject } from 'rxjs'; +import { filter, takeUntil, takeWhile, distinctUntilChanged } from 'rxjs/operators'; +import { Subject, BehaviorSubject, Observable } from 'rxjs'; import { NbAdjustableConnectedPositionStrategy, @@ -9,7 +9,7 @@ import { import { NbRenderableContainer } from '../overlay-container'; import { createContainer, NbOverlayContent, NbOverlayService, patch } from '../overlay-service'; -import { NbOverlayRef, NbOverlayContainer } from '../mapping'; +import { NbOverlayRef, NbOverlayContainer, NbOverlayConfig } from '../mapping'; export interface NbDynamicOverlayController { show(); @@ -27,14 +27,20 @@ export class NbDynamicOverlay { protected context: Object = {}; protected content: NbOverlayContent; protected positionStrategy: NbAdjustableConnectedPositionStrategy; + protected overlayConfig: NbOverlayConfig = {}; protected positionStrategyChange$ = new Subject(); + protected isShown$ = new BehaviorSubject(false); protected alive = true; get isAttached(): boolean { return this.ref && this.ref.hasAttached(); } + get isShown(): Observable { + return this.isShown$.pipe(distinctUntilChanged()); + } + constructor( protected overlay: NbOverlayService, protected componentFactoryResolver: ComponentFactoryResolver, @@ -45,11 +51,13 @@ export class NbDynamicOverlay { create(componentType: Type, content: NbOverlayContent, context: Object, - positionStrategy: NbAdjustableConnectedPositionStrategy) { + positionStrategy: NbAdjustableConnectedPositionStrategy, + overlayConfig: NbOverlayConfig = {}) { this.setContentAndContext(content, context); this.setComponent(componentType); this.setPositionStrategy(positionStrategy); + this.setOverlayConfig(overlayConfig); return this; } @@ -82,11 +90,10 @@ export class NbDynamicOverlay { this.componentType = componentType; // in case the component is shown we recreate it and show it back - if (this.ref && this.isAttached) { - this.dispose(); + const wasAttached = this.isAttached; + this.disposeOverlayRef(); + if (wasAttached) { this.show(); - } else if (this.ref && !this.isAttached) { - this.dispose(); } } @@ -108,6 +115,16 @@ export class NbDynamicOverlay { } } + setOverlayConfig(overlayConfig: NbOverlayConfig) { + this.overlayConfig = overlayConfig; + + const wasAttached = this.isAttached; + this.disposeOverlayRef(); + if (wasAttached) { + this.show(); + } + } + show() { if (!this.ref) { this.createOverlay(); @@ -120,6 +137,8 @@ export class NbDynamicOverlay { this.disposeOverlayRef(); return this.show(); } + + this.isShown$.next(true); } hide() { @@ -129,6 +148,8 @@ export class NbDynamicOverlay { this.ref.detach(); this.container = null; + + this.isShown$.next(false); } toggle() { @@ -143,6 +164,8 @@ export class NbDynamicOverlay { this.alive = false; this.hide(); this.disposeOverlayRef(); + this.isShown$.complete(); + this.positionStrategyChange$.complete(); } getContainer() { @@ -153,6 +176,7 @@ export class NbDynamicOverlay { this.ref = this.overlay.create({ positionStrategy: this.positionStrategy, scrollStrategy: this.overlay.scrollStrategies.reposition(), + ...this.overlayConfig, }); this.updatePositionWhenStable(); } diff --git a/src/framework/theme/components/popover/popover.directive.ts b/src/framework/theme/components/popover/popover.directive.ts index 5c9b21b27b..eff68fb297 100644 --- a/src/framework/theme/components/popover/popover.directive.ts +++ b/src/framework/theme/components/popover/popover.directive.ts @@ -10,7 +10,10 @@ import { ElementRef, Input, OnChanges, - OnDestroy, OnInit, + OnDestroy, + OnInit, + Output, + EventEmitter, } from '@angular/core'; import { NbDynamicOverlay, NbDynamicOverlayController } from '../cdk/overlay/dynamic/dynamic-overlay'; @@ -19,6 +22,8 @@ import { NbAdjustment, NbPosition } from '../cdk/overlay/overlay-position'; import { NbOverlayContent } from '../cdk/overlay/overlay-service'; import { NbTrigger } from '../cdk/overlay/overlay-trigger'; import { NbPopoverComponent } from './popover.component'; +import { takeUntil, skip } from 'rxjs/operators'; +import { Subject } from 'rxjs'; /** @@ -107,10 +112,15 @@ import { NbPopoverComponent } from './popover.component'; * */ @Directive({ selector: '[nbPopover]', + exportAs: 'nbPopover', providers: [NbDynamicOverlayHandler, NbDynamicOverlay], }) export class NbPopoverDirective implements NbDynamicOverlayController, OnChanges, AfterViewInit, OnDestroy, OnInit { + protected popoverComponent = NbPopoverComponent; + protected dynamicOverlay: NbDynamicOverlay; + protected destroy$ = new Subject(); + /** * Popover content which will be rendered in NbArrowedOverlayContainerComponent. * Available content: template ref, component and any primitive. @@ -146,16 +156,30 @@ export class NbPopoverDirective implements NbDynamicOverlayController, OnChanges @Input('nbPopoverTrigger') trigger: NbTrigger = NbTrigger.CLICK; - private dynamicOverlay: NbDynamicOverlay; + /** + * Sets popover offset + * */ + @Input('nbPopoverOffset') + offset = 15; + + @Input('nbPopoverClass') + popoverClass: string = ''; - constructor(private hostRef: ElementRef, - private dynamicOverlayHandler: NbDynamicOverlayHandler) { + @Output() + nbPopoverShowStateChange = new EventEmitter<{ isShown: boolean }>(); + + get isShown(): boolean { + return !!(this.dynamicOverlay && this.dynamicOverlay.isAttached); + } + + constructor(protected hostRef: ElementRef, + protected dynamicOverlayHandler: NbDynamicOverlayHandler) { } ngOnInit() { this.dynamicOverlayHandler .host(this.hostRef) - .componentType(NbPopoverComponent); + .componentType(this.popoverComponent); } ngOnChanges() { @@ -165,6 +189,13 @@ export class NbPopoverDirective implements NbDynamicOverlayController, OnChanges ngAfterViewInit() { this.dynamicOverlay = this.configureDynamicOverlay() .build(); + + this.dynamicOverlay.isShown + .pipe( + skip(1), + takeUntil(this.destroy$), + ) + .subscribe((isShown: boolean) => this.nbPopoverShowStateChange.emit({ isShown })); } rebuild() { @@ -186,14 +217,18 @@ export class NbPopoverDirective implements NbDynamicOverlayController, OnChanges ngOnDestroy() { this.dynamicOverlayHandler.destroy(); + this.destroy$.next(); + this.destroy$.complete(); } protected configureDynamicOverlay() { return this.dynamicOverlayHandler .position(this.position) .trigger(this.trigger) + .offset(this.offset) .adjustment(this.adjustment) .content(this.content) - .context(this.context); + .context(this.context) + .overlayConfig({ panelClass: this.popoverClass }); } } diff --git a/src/framework/theme/components/popover/popover.spec.ts b/src/framework/theme/components/popover/popover.spec.ts index b0f26a12ac..d1e6ba3224 100644 --- a/src/framework/theme/components/popover/popover.spec.ts +++ b/src/framework/theme/components/popover/popover.spec.ts @@ -12,6 +12,9 @@ import { NbTrigger } from '../cdk/overlay/overlay-trigger'; import { NbPopoverDirective } from './popover.directive'; import { NbPopoverComponent } from './popover.component'; import { NbPopoverModule } from './popover.module'; +import createSpy = jasmine.createSpy; +import { Subject } from 'rxjs'; +import { NbOverlayConfig } from '@nebular/theme/components/cdk/overlay/mapping'; @Component({ selector: 'nb-popover-component-content-test', @@ -26,7 +29,7 @@ export class NbPopoverComponentContentTestComponent { template: ` - + `, @@ -34,6 +37,8 @@ export class NbPopoverComponentContentTestComponent { export class NbPopoverDefaultTestComponent { @ViewChild('button', { static: false }) button: ElementRef; @ViewChild(NbPopoverDirective, { static: false }) popover: NbPopoverDirective; + + popoverClass = ''; } @Component({ @@ -82,11 +87,13 @@ export class NbPopoverInstanceTestComponent { @ViewChild(TemplateRef, { static: false }) template: TemplateRef; } +const dynamicOverlayIsShow$ = new Subject(); const dynamicOverlay = { show() {}, hide() {}, toggle() {}, destroy() {}, + isShown: dynamicOverlayIsShow$, }; export class NbDynamicOverlayHandlerMock { @@ -97,6 +104,8 @@ export class NbDynamicOverlayHandlerMock { _trigger: NbTrigger = NbTrigger.NOOP; _position: NbPosition = NbPosition.TOP; _adjustment: NbAdjustment = NbAdjustment.NOOP; + _offset = 15; + _overlayConfig: NbOverlayConfig = {}; constructor() { } @@ -116,6 +125,11 @@ export class NbDynamicOverlayHandlerMock { return this; } + offset(offset: number) { + this._offset = offset; + return this; + } + adjustment(adjustment: NbAdjustment) { this._adjustment = adjustment; return this; @@ -136,6 +150,11 @@ export class NbDynamicOverlayHandlerMock { return this; } + overlayConfig(overlayConfig: NbOverlayConfig) { + this._overlayConfig = overlayConfig; + return this; + } + build() { return dynamicOverlay; } @@ -246,6 +265,56 @@ describe('Directive: NbPopoverDirective', () => { expect(templatePopover.textContent).toContain('hello world'); }); + it('should emit show state change event when shows up', () => { + fixture = TestBed.createComponent(NbPopoverDefaultTestComponent); + fixture.detectChanges(); + const popover: NbPopoverDirective = fixture.componentInstance.popover; + + const stateChangeSpy = createSpy('stateChangeSpy'); + popover.nbPopoverShowStateChange.subscribe(stateChangeSpy); + + popover.show(); + fixture.detectChanges(); + + expect(stateChangeSpy).toHaveBeenCalledTimes(1); + expect(stateChangeSpy).toHaveBeenCalledWith(jasmine.objectContaining({ isShown: true })); + }); + + it('should emit show state change event when hides', () => { + fixture = TestBed.createComponent(NbPopoverDefaultTestComponent); + fixture.detectChanges(); + const popover: NbPopoverDirective = fixture.componentInstance.popover; + popover.show(); + fixture.detectChanges(); + + const stateChangeSpy = createSpy('stateChangeSpy'); + popover.nbPopoverShowStateChange.subscribe(stateChangeSpy); + + popover.hide(); + fixture.detectChanges(); + + expect(stateChangeSpy).toHaveBeenCalledTimes(1); + expect(stateChangeSpy).toHaveBeenCalledWith(jasmine.objectContaining({ isShown: false })); + }); + + it('should set isShown to false when hidden', () => { + fixture = TestBed.createComponent(NbPopoverDefaultTestComponent); + fixture.detectChanges(); + const popover: NbPopoverDirective = fixture.componentInstance.popover; + + expect(popover.isShown).toEqual(false); + }); + + it('should set isShown to true when shown', () => { + fixture = TestBed.createComponent(NbPopoverDefaultTestComponent); + fixture.detectChanges(); + const popover: NbPopoverDirective = fixture.componentInstance.popover; + popover.show(); + fixture.detectChanges(); + + expect(popover.isShown).toEqual(true); + }); + }); describe('mocked services', () => { @@ -336,6 +405,17 @@ describe('Directive: NbPopoverDirective', () => { expect(hideSpy).toHaveBeenCalledTimes(1); expect(toggleSpy).toHaveBeenCalledTimes(1); }); + + it('should set overlay config', () => { + const popoverClass = 'custom-popover-class'; + const overlayConfigSpy = spyOn(overlayHandler, 'overlayConfig').and.callThrough(); + + fixture = TestBed.createComponent(NbPopoverDefaultTestComponent); + fixture.componentInstance.popoverClass = popoverClass; + fixture.detectChanges(); + + expect(overlayConfigSpy).toHaveBeenCalledWith(jasmine.objectContaining({ panelClass: popoverClass })); + }); }); describe('binding popover', () => { diff --git a/src/framework/theme/components/tooltip/tooltip.directive.ts b/src/framework/theme/components/tooltip/tooltip.directive.ts index 63281a1807..77cdd5bdca 100644 --- a/src/framework/theme/components/tooltip/tooltip.directive.ts +++ b/src/framework/theme/components/tooltip/tooltip.directive.ts @@ -4,7 +4,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { AfterViewInit, Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; +import { + AfterViewInit, + Directive, + ElementRef, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + EventEmitter, +} from '@angular/core'; +import { skip, takeUntil } from 'rxjs/operators'; +import { Subject } from 'rxjs'; import { NbComponentStatus } from '../component-status'; import { NbAdjustment, NbPosition } from '../cdk/overlay/overlay-position'; @@ -56,12 +68,17 @@ import { NbIconConfig } from '../icon/icon.component'; */ @Directive({ selector: '[nbTooltip]', + exportAs: 'nbTooltip', providers: [NbDynamicOverlayHandler, NbDynamicOverlay], }) export class NbTooltipDirective implements OnInit, OnChanges, AfterViewInit, OnDestroy { - context: Object = {}; + protected destroy$ = new Subject(); + protected tooltipComponent = NbTooltipComponent; + protected dynamicOverlay: NbDynamicOverlay; + protected offset = 8; + context: Object = {}; /** * Tooltip message */ @@ -81,6 +98,9 @@ export class NbTooltipDirective implements OnInit, OnChanges, AfterViewInit, OnD @Input('nbTooltipAdjustment') adjustment: NbAdjustment = NbAdjustment.CLOCKWISE; + @Input('nbTooltipClass') + tooltipClass: string = ''; + /** * Accepts icon name or icon config object * @param {string | NbIconConfig} icon name or config object @@ -106,17 +126,22 @@ export class NbTooltipDirective implements OnInit, OnChanges, AfterViewInit, OnD @Input('nbTooltipTrigger') trigger: NbTrigger = NbTrigger.HINT; - private dynamicOverlay: NbDynamicOverlay; + @Output() + nbTooltipShowStateChange = new EventEmitter<{ isShown: boolean }>(); - constructor(private hostRef: ElementRef, - private dynamicOverlayHandler: NbDynamicOverlayHandler) { + get isShown(): boolean { + return !!(this.dynamicOverlay && this.dynamicOverlay.isAttached); + } + + constructor(protected hostRef: ElementRef, + protected dynamicOverlayHandler: NbDynamicOverlayHandler) { } ngOnInit() { this.dynamicOverlayHandler .host(this.hostRef) - .componentType(NbTooltipComponent) - .offset(8); + .componentType(this.tooltipComponent) + .offset(this.offset); } ngOnChanges() { @@ -126,6 +151,13 @@ export class NbTooltipDirective implements OnInit, OnChanges, AfterViewInit, OnD ngAfterViewInit() { this.dynamicOverlay = this.configureDynamicOverlay() .build(); + + this.dynamicOverlay.isShown + .pipe( + skip(1), + takeUntil(this.destroy$), + ) + .subscribe((isShown: boolean) => this.nbTooltipShowStateChange.emit({ isShown })); } rebuild() { @@ -147,6 +179,8 @@ export class NbTooltipDirective implements OnInit, OnChanges, AfterViewInit, OnD ngOnDestroy() { this.dynamicOverlayHandler.destroy(); + this.destroy$.next(); + this.destroy$.complete(); } protected configureDynamicOverlay() { @@ -155,6 +189,7 @@ export class NbTooltipDirective implements OnInit, OnChanges, AfterViewInit, OnD .trigger(this.trigger) .adjustment(this.adjustment) .content(this.content) - .context(this.context); + .context(this.context) + .overlayConfig({ panelClass: this.tooltipClass }); } } diff --git a/src/framework/theme/components/tooltip/tooltip.spec.ts b/src/framework/theme/components/tooltip/tooltip.spec.ts index 16ab92aec0..be2c1c8c40 100644 --- a/src/framework/theme/components/tooltip/tooltip.spec.ts +++ b/src/framework/theme/components/tooltip/tooltip.spec.ts @@ -14,6 +14,9 @@ import { NbTooltipDirective } from './tooltip.directive'; import { NbTooltipModule } from './tooltip.module'; import { NbTooltipComponent } from './tooltip.component'; import { NbIconLibraries } from '../icon/icon-libraries'; +import { Subject } from 'rxjs'; +import createSpy = jasmine.createSpy; +import { NbOverlayConfig } from '@nebular/theme/components/cdk/overlay/mapping'; @Component({ selector: 'nb-tooltip-default-test', @@ -40,7 +43,8 @@ export class NbTooltipDefaultTestComponent { [nbTooltipPlacement]="position" [nbTooltipAdjustment]="adjustment" [nbTooltipStatus]="status" - [nbTooltipIcon]="icon"> + [nbTooltipIcon]="icon" + [nbTooltipClass]="tooltipClass"> @@ -55,6 +59,7 @@ export class NbTooltipBindingsTestComponent { @Input() trigger = NbTrigger.CLICK; @Input() position = NbPosition.TOP; @Input() adjustment = NbAdjustment.CLOCKWISE; + tooltipClass = ''; } @Component({ @@ -74,11 +79,13 @@ export class NbTooltipInstanceTestComponent { @ViewChild('button', { static: false }) button: ElementRef; } +const dynamicOverlayIsShow$ = new Subject(); const dynamicOverlay = { show() {}, hide() {}, toggle() {}, destroy() {}, + isShown: dynamicOverlayIsShow$, }; export class NbDynamicOverlayHandlerMock { @@ -90,6 +97,7 @@ export class NbDynamicOverlayHandlerMock { _position: NbPosition = NbPosition.TOP; _adjustment: NbAdjustment = NbAdjustment.NOOP; _offset: number; + _overlayConfig: NbOverlayConfig = {}; constructor() { } @@ -134,6 +142,11 @@ export class NbDynamicOverlayHandlerMock { return this; } + overlayConfig(overlayConfig: NbOverlayConfig) { + this._overlayConfig = overlayConfig; + return this; + } + build() { return dynamicOverlay; } @@ -260,6 +273,56 @@ describe('Directive: NbTooltipDirective', () => { expect(iconContainer.className).toContain('status-danger'); }); + it('should emit show state change event when shows up', () => { + fixture = TestBed.createComponent(NbTooltipDefaultTestComponent); + fixture.detectChanges(); + const tooltip: NbTooltipDirective = fixture.componentInstance.tooltip; + + const stateChangeSpy = createSpy('stateChangeSpy'); + tooltip.nbTooltipShowStateChange.subscribe(stateChangeSpy); + + tooltip.show(); + fixture.detectChanges(); + + expect(stateChangeSpy).toHaveBeenCalledTimes(1); + expect(stateChangeSpy).toHaveBeenCalledWith(jasmine.objectContaining({ isShown: true })); + }); + + it('should emit show state change event when hides', () => { + fixture = TestBed.createComponent(NbTooltipDefaultTestComponent); + fixture.detectChanges(); + const tooltip: NbTooltipDirective = fixture.componentInstance.tooltip; + tooltip.show(); + fixture.detectChanges(); + + const stateChangeSpy = createSpy('stateChangeSpy'); + tooltip.nbTooltipShowStateChange.subscribe(stateChangeSpy); + + tooltip.hide(); + fixture.detectChanges(); + + expect(stateChangeSpy).toHaveBeenCalledTimes(1); + expect(stateChangeSpy).toHaveBeenCalledWith(jasmine.objectContaining({ isShown: false })); + }); + + it('should set isShown to false when hidden', () => { + fixture = TestBed.createComponent(NbTooltipDefaultTestComponent); + fixture.detectChanges(); + const tooltip: NbTooltipDirective = fixture.componentInstance.tooltip; + + expect(tooltip.isShown).toEqual(false); + }); + + it('should set isShown to true when shown', () => { + fixture = TestBed.createComponent(NbTooltipDefaultTestComponent); + fixture.detectChanges(); + const tooltip: NbTooltipDirective = fixture.componentInstance.tooltip; + tooltip.show(); + fixture.detectChanges(); + + expect(tooltip.isShown).toEqual(true); + }); + }); describe('mocked services', () => { @@ -419,6 +482,17 @@ describe('Directive: NbTooltipDirective', () => { expect(contentSpy).toHaveBeenCalledTimes(3); expect(contentSpy).toHaveBeenCalledWith('new string'); }); + + it('should set overlay config', () => { + const tooltipClass = 'custom-popover-class'; + const overlayConfigSpy = spyOn(overlayHandler, 'overlayConfig').and.callThrough(); + + fixture = TestBed.createComponent(NbTooltipBindingsTestComponent); + fixture.componentInstance.tooltipClass = tooltipClass; + fixture.detectChanges(); + + expect(overlayConfigSpy).toHaveBeenCalledWith(jasmine.objectContaining({ panelClass: tooltipClass })); + }); }); describe('instance tooltip', () => {