Skip to content

Commit

Permalink
feat(popover): add animations
Browse files Browse the repository at this point in the history
  • Loading branch information
fbasso authored and maxokorokov committed Jul 1, 2020
1 parent 2b1cc56 commit b7f12e6
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 44 deletions.
5 changes: 4 additions & 1 deletion src/popover/popover-config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import {NgbPopoverConfig} from './popover-config';
import {NgbConfig} from '../ngb-config';

describe('ngb-popover-config', () => {
it('should have sensible default values', () => {
const config = new NgbPopoverConfig();
const ngbConfig = new NgbConfig();
const config = new NgbPopoverConfig(ngbConfig);

expect(config.animation).toBe(ngbConfig.animation);
expect(config.autoClose).toBe(true);
expect(config.placement).toBe('auto');
expect(config.triggers).toBe('click');
Expand Down
4 changes: 4 additions & 0 deletions src/popover/popover-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Injectable} from '@angular/core';
import {PlacementArray} from '../util/positioning';
import {NgbConfig} from '../ngb-config';

/**
* A configuration service for the [`NgbPopover`](#/components/popover/api#NgbPopover) component.
Expand All @@ -9,6 +10,7 @@ import {PlacementArray} from '../util/positioning';
*/
@Injectable({providedIn: 'root'})
export class NgbPopoverConfig {
animation: boolean;
autoClose: boolean | 'inside' | 'outside' = true;
placement: PlacementArray = 'auto';
triggers = 'click';
Expand All @@ -17,4 +19,6 @@ export class NgbPopoverConfig {
popoverClass: string;
openDelay = 0;
closeDelay = 0;

constructor(ngbConfig: NgbConfig) { this.animation = ngbConfig.animation; }
}
41 changes: 30 additions & 11 deletions src/popover/popover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ let nextId = 0;
selector: 'ngb-popover-window',
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
host: {'[class]': '"popover" + (popoverClass ? " " + popoverClass : "")', 'role': 'tooltip', '[id]': 'id'},
host: {
'[class]': '"popover" + (popoverClass ? " " + popoverClass : "")',
'[class.fade]': 'animation',
'role': 'tooltip',
'[id]': 'id'
},
template: `
<div class="arrow"></div>
<h3 class="popover-header" *ngIf="title != null">
Expand All @@ -48,6 +53,7 @@ let nextId = 0;
styleUrls: ['./popover.scss']
})
export class NgbPopoverWindow {
@Input() animation: boolean;
@Input() title: undefined | string | TemplateRef<any>;
@Input() id: string;
@Input() popoverClass: string;
Expand All @@ -63,6 +69,11 @@ export class NgbPopoverWindow {
export class NgbPopover implements OnInit, OnDestroy, OnChanges {
static ngAcceptInputType_autoClose: boolean | string;

/**
* If `true`, popover opening and closing will be animated.
*/
@Input() animation: boolean;

/**
* Indicates whether the popover should be closed on `Escape` key and inside/outside clicks:
*
Expand Down Expand Up @@ -149,12 +160,14 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
@Input() closeDelay: number;

/**
* An event emitted when the popover is shown. Contains no payload.
* An event emitted when the popover opening animation has finished. Contains no payload.
*/
@Output() shown = new EventEmitter<void>();

/**
* An event emitted when the popover is hidden. Contains no payload.
* An event emitted when the popover closing animation has finished. Contains no payload.
*
* At this point popover is not in the DOM anymore.
*/
@Output() hidden = new EventEmitter<void>();

Expand All @@ -178,6 +191,7 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
componentFactoryResolver: ComponentFactoryResolver, viewContainerRef: ViewContainerRef, config: NgbPopoverConfig,
private _ngZone: NgZone, @Inject(DOCUMENT) private _document: any, private _changeDetector: ChangeDetectorRef,
applicationRef: ApplicationRef) {
this.animation = config.animation;
this.autoClose = config.autoClose;
this.placement = config.placement;
this.triggers = config.triggers;
Expand All @@ -187,7 +201,8 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
this.openDelay = config.openDelay;
this.closeDelay = config.closeDelay;
this._popupService = new PopupService<NgbPopoverWindow>(
NgbPopoverWindow, injector, viewContainerRef, _renderer, componentFactoryResolver, applicationRef);
NgbPopoverWindow, injector, viewContainerRef, _renderer, this._ngZone, componentFactoryResolver,
applicationRef);

this._zoneSubscription = _ngZone.onStable.subscribe(() => {
if (this._windowRef) {
Expand All @@ -206,7 +221,9 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
*/
open(context?: any) {
if (!this._windowRef && !this._isDisabled()) {
this._windowRef = this._popupService.open(this.ngbPopover, context);
const {windowRef, transition$} = this._popupService.open(this.ngbPopover, context, this.animation);
this._windowRef = windowRef;
this._windowRef.instance.animation = this.animation;
this._windowRef.instance.title = this.popoverTitle;
this._windowRef.instance.context = context;
this._windowRef.instance.popoverClass = this.popoverClass;
Expand All @@ -233,7 +250,8 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
ngbAutoClose(
this._ngZone, this._document, this.autoClose, () => this.close(), this.hidden,
[this._windowRef.location.nativeElement]);
this.shown.emit();

transition$.subscribe(() => this.shown.emit());
}
}

Expand All @@ -242,13 +260,14 @@ export class NgbPopover implements OnInit, OnDestroy, OnChanges {
*
* This is considered to be a "manual" triggering of the popover.
*/
close(): void {
close() {
if (this._windowRef) {
this._renderer.removeAttribute(this._elementRef.nativeElement, 'aria-describedby');
this._popupService.close();
this._windowRef = null;
this.hidden.emit();
this._changeDetector.markForCheck();
this._popupService.close(this.animation).subscribe(() => {
this._windowRef = null;
this.hidden.emit();
this._changeDetector.markForCheck();
});
}
}

Expand Down
17 changes: 10 additions & 7 deletions src/tooltip/tooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ export class NgbTooltip implements OnInit, OnDestroy, OnChanges {
this.openDelay = config.openDelay;
this.closeDelay = config.closeDelay;
this._popupService = new PopupService<NgbTooltipWindow>(
NgbTooltipWindow, injector, viewContainerRef, _renderer, componentFactoryResolver, applicationRef);
NgbTooltipWindow, injector, viewContainerRef, _renderer, this._ngZone, componentFactoryResolver,
applicationRef);

this._zoneSubscription = _ngZone.onStable.subscribe(() => {
if (this._windowRef) {
Expand Down Expand Up @@ -188,7 +189,8 @@ export class NgbTooltip implements OnInit, OnDestroy, OnChanges {
*/
open(context?: any) {
if (!this._windowRef && this._ngbTooltip && !this.disableTooltip) {
this._windowRef = this._popupService.open(this._ngbTooltip, context);
const {windowRef, transition$} = this._popupService.open(this._ngbTooltip, context);
this._windowRef = windowRef;
this._windowRef.instance.tooltipClass = this.tooltipClass;
this._windowRef.instance.id = this._ngbTooltipWindowId;

Expand All @@ -214,7 +216,7 @@ export class NgbTooltip implements OnInit, OnDestroy, OnChanges {
this._ngZone, this._document, this.autoClose, () => this.close(), this.hidden,
[this._windowRef.location.nativeElement]);

this.shown.emit();
transition$.subscribe(() => this.shown.emit());
}
}

Expand All @@ -226,10 +228,11 @@ export class NgbTooltip implements OnInit, OnDestroy, OnChanges {
close(): void {
if (this._windowRef != null) {
this._renderer.removeAttribute(this._elementRef.nativeElement, 'aria-describedby');
this._popupService.close();
this._windowRef = null;
this.hidden.emit();
this._changeDetector.markForCheck();
this._popupService.close().subscribe(() => {
this._windowRef = null;
this.hidden.emit();
this._changeDetector.markForCheck();
});
}
}

Expand Down
15 changes: 9 additions & 6 deletions src/typeahead/typeahead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ export class NgbTypeahead implements ControlValueAccessor,
this._resubscribeTypeahead = new BehaviorSubject(null);

this._popupService = new PopupService<NgbTypeaheadWindow>(
NgbTypeaheadWindow, injector, viewContainerRef, _renderer, componentFactoryResolver, applicationRef);
NgbTypeaheadWindow, injector, viewContainerRef, _renderer, this._ngZone, componentFactoryResolver,
applicationRef);

this._zoneSubscription = ngZone.onStable.subscribe(() => {
if (this.isPopupOpen()) {
Expand Down Expand Up @@ -304,7 +305,8 @@ export class NgbTypeahead implements ControlValueAccessor,
private _openPopup() {
if (!this.isPopupOpen()) {
this._inputValueBackup = this._elementRef.nativeElement.value;
this._windowRef = this._popupService.open();
const {windowRef} = this._popupService.open();
this._windowRef = windowRef;
this._windowRef.instance.id = this.popupId;
this._windowRef.instance.selectEvent.subscribe((result: any) => this._selectResultClosePopup(result));
this._windowRef.instance.activeChangeEvent.subscribe((activeId: string) => this.activeDescendant = activeId);
Expand All @@ -322,10 +324,11 @@ export class NgbTypeahead implements ControlValueAccessor,
}

private _closePopup() {
this._closed$.next();
this._popupService.close();
this._windowRef = null;
this.activeDescendant = null;
this._popupService.close().subscribe(() => {
this._closed$.next();
this._windowRef = null;
this.activeDescendant = null;
});
}

private _selectResult(result: any) {
Expand Down
61 changes: 42 additions & 19 deletions src/util/popup.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import {
ApplicationRef,
ComponentFactoryResolver,
ComponentRef,
Injector,
NgZone,
Renderer2,
TemplateRef,
ViewRef,
ViewContainerRef,
Renderer2,
ComponentRef,
ComponentFactoryResolver,
ApplicationRef
ViewRef
} from '@angular/core';

import {Observable, of} from 'rxjs';
import {mergeMap, take, tap} from 'rxjs/operators';

import {ngbRunTransition} from './transition/ngbTransition';

export class ContentRef {
constructor(public nodes: any[], public viewRef?: ViewRef, public componentRef?: ComponentRef<any>) {}
}
Expand All @@ -19,31 +25,48 @@ export class PopupService<T> {

constructor(
private _type: any, private _injector: Injector, private _viewContainerRef: ViewContainerRef,
private _renderer: Renderer2, private _componentFactoryResolver: ComponentFactoryResolver,
private _applicationRef: ApplicationRef) {}
private _renderer: Renderer2, private _ngZone: NgZone,
private _componentFactoryResolver: ComponentFactoryResolver, private _applicationRef: ApplicationRef) {}

open(content?: string | TemplateRef<any>, context?: any): ComponentRef<T> {
open(content?: string | TemplateRef<any>, context?: any, animation = false):
{windowRef: ComponentRef<T>, transition$: Observable<void>} {
if (!this._windowRef) {
this._contentRef = this._getContentRef(content, context);
this._windowRef = this._viewContainerRef.createComponent(
this._componentFactoryResolver.resolveComponentFactory<T>(this._type), this._viewContainerRef.length,
this._injector, this._contentRef.nodes);
}

return this._windowRef;
}
const {nativeElement} = this._windowRef.location;
const onStable$ = this._ngZone.onStable.asObservable().pipe(take(1));
const transition$ = onStable$.pipe(
mergeMap(
() => ngbRunTransition(
nativeElement, ({classList}) => classList.add('show'), {animation, runningTransition: 'continue'})));

close() {
if (this._windowRef) {
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._windowRef.hostView));
this._windowRef = null;
return {windowRef: this._windowRef, transition$};
}

if (this._contentRef?.viewRef) {
this._applicationRef.detachView(this._contentRef.viewRef);
this._contentRef.viewRef.destroy();
this._contentRef = null;
}
close(animation = false): Observable<void> {
if (!this._windowRef) {
return of(undefined);
}

return ngbRunTransition(
this._windowRef.location.nativeElement, ({classList}) => classList.remove('show'),
{animation, runningTransition: 'stop'})
.pipe(tap(() => {
if (this._windowRef) {
// this is required because of the container='body' option
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._windowRef.hostView));
this._windowRef = null;
}
if (this._contentRef?.viewRef) {
this._applicationRef.detachView(this._contentRef.viewRef);
this._contentRef.viewRef.destroy();
this._contentRef = null;
}
}));
}

private _getContentRef(content?: string | TemplateRef<any>, context?: any): ContentRef {
Expand Down

0 comments on commit b7f12e6

Please sign in to comment.