diff --git a/src/app/components/contextmenu/contextmenu.ts b/src/app/components/contextmenu/contextmenu.ts index 5798d21c76a..bcf36634f97 100755 --- a/src/app/components/contextmenu/contextmenu.ts +++ b/src/app/components/contextmenu/contextmenu.ts @@ -342,7 +342,7 @@ export class ContextMenu implements AfterViewInit, OnDestroy { if (this.global) { const documentTarget: any = this.el ? this.el.nativeElement.ownerDocument : 'document'; this.triggerEventListener = this.renderer.listen(documentTarget, this.triggerEvent, (event) => { - if(this.containerViewChild && this.containerViewChild.nativeElement.style.display !== 'none') { + if (this.containerViewChild && this.containerViewChild.nativeElement.style.display !== 'none') { this.hide(); } this.show(event); @@ -350,7 +350,7 @@ export class ContextMenu implements AfterViewInit, OnDestroy { }); } else if (this.target) { this.triggerEventListener = this.renderer.listen(this.target, this.triggerEvent, (event) => { - if(this.containerViewChild && this.containerViewChild.nativeElement.style.display !== 'none') { + if (this.containerViewChild && this.containerViewChild.nativeElement.style.display !== 'none') { this.hide(); } this.show(event); diff --git a/src/app/components/tabview/tabview.ts b/src/app/components/tabview/tabview.ts index 6996e39e4b5..7e6cc9a0f9b 100755 --- a/src/app/components/tabview/tabview.ts +++ b/src/app/components/tabview/tabview.ts @@ -1,4 +1,4 @@ -import { CommonModule, isPlatformBrowser } from '@angular/common'; +import { CommonModule, isPlatformBrowser, DOCUMENT } from '@angular/common'; import { AfterContentInit, AfterViewChecked, @@ -20,7 +20,10 @@ import { ViewChild, ViewContainerRef, ViewEncapsulation, - forwardRef + forwardRef, + AfterViewInit, + NgZone, + Self } from '@angular/core'; import { BlockableUI, PrimeTemplate, SharedModule } from 'primeng/api'; import { DomHandler } from 'primeng/dom'; @@ -29,7 +32,10 @@ import { ChevronRightIcon } from 'primeng/icons/chevronright'; import { TimesIcon } from 'primeng/icons/times'; import { RippleModule } from 'primeng/ripple'; import { TooltipModule } from 'primeng/tooltip'; -import { Subscription } from 'rxjs'; +import { filter, fromEvent } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { OnDestroyService } from '../utils/on-destroy.service'; +import { outsideZone } from '../utils/outside-zone-operator'; import { TabViewChangeEvent, TabViewCloseEvent } from './tabview.interface'; let idx: number = 0; @@ -248,7 +254,6 @@ export class TabPanel implements AfterContentInit, OnDestroy { role="tab" class="p-tabview-nav-link" [attr.id]="tab.id + '-label'" - [attr.aria-selected]="tab.selected" [attr.aria-controls]="tab.id" [pTooltip]="tab.tooltip" [tooltipPosition]="tab.tooltipPosition" @@ -298,9 +303,10 @@ export class TabPanel implements AfterContentInit, OnDestroy { styleUrls: ['./tabview.css'], host: { class: 'p-element' - } + }, + providers: [OnDestroyService] }) -export class TabView implements AfterContentInit, AfterViewChecked, OnDestroy, BlockableUI { +export class TabView implements AfterContentInit, AfterViewInit, AfterViewChecked, BlockableUI { /** * Inline style of the component. * @group Props @@ -392,18 +398,16 @@ export class TabView implements AfterContentInit, AfterViewChecked, OnDestroy, B forwardIsDisabled: boolean = false; - private tabChangesSubscription!: Subscription; - nextIconTemplate: TemplateRef | undefined; previousIconTemplate: TemplateRef | undefined; - constructor(@Inject(PLATFORM_ID) private platformId: any, public el: ElementRef, public cd: ChangeDetectorRef) {} + constructor(@Inject(PLATFORM_ID) private platformId: any, public el: ElementRef, public cd: ChangeDetectorRef, private zone: NgZone, @Self() private destroy$: OnDestroyService, @Inject(DOCUMENT) private documentRef: Document) {} ngAfterContentInit() { this.initTabs(); - this.tabChangesSubscription = (this.tabPanels as QueryList).changes.subscribe((_) => { + (this.tabPanels as QueryList).changes.pipe(takeUntil(this.destroy$)).subscribe((_) => { this.initTabs(); }); @@ -420,6 +424,11 @@ export class TabView implements AfterContentInit, AfterViewChecked, OnDestroy, B }); } + ngAfterViewInit(): void { + this.initButtonState(); + this.listenWindowResize(); + } + ngAfterViewChecked() { if (isPlatformBrowser(this.platformId)) { if (this.tabChanged) { @@ -429,12 +438,6 @@ export class TabView implements AfterContentInit, AfterViewChecked, OnDestroy, B } } - ngOnDestroy(): void { - if (this.tabChangesSubscription) { - this.tabChangesSubscription.unsubscribe(); - } - } - initTabs(): void { this.tabs = (this.tabPanels as QueryList).toArray(); let selectedTab: TabPanel = this.findSelectedTab() as TabPanel; @@ -590,6 +593,39 @@ export class TabView implements AfterContentInit, AfterViewChecked, OnDestroy, B content.scrollLeft = pos >= lastPos ? lastPos : pos; } + + private initButtonState(): void { + if (this.scrollable) { + // We have to wait for the rendering and then retrieve the actual size element from the DOM. + // in future `Promise.resolve` can be changed to `queueMicrotask` (if ie11 support will be dropped) + Promise.resolve().then(() => { + this.updateButtonState(); + this.cd.markForCheck(); + }); + } + } + + private listenWindowResize(): void { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + fromEvent(this.documentRef.defaultView, 'resize', { passive: true }) + .pipe( + outsideZone(this.zone), + filter(() => this.scrollable), + takeUntil(this.destroy$) + ) + .subscribe(() => { + const prevBackwardIsDisabled = this.backwardIsDisabled; + const prevForwardIsDisabled = this.forwardIsDisabled; + this.updateButtonState(); + + if (this.forwardIsDisabled !== prevForwardIsDisabled || this.backwardIsDisabled !== prevBackwardIsDisabled) { + this.cd.detectChanges(); + } + }); + } } @NgModule({ diff --git a/src/app/components/utils/on-destroy.service.ts b/src/app/components/utils/on-destroy.service.ts new file mode 100644 index 00000000000..9aa73206fbb --- /dev/null +++ b/src/app/components/utils/on-destroy.service.ts @@ -0,0 +1,33 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { Subject } from 'rxjs'; + +/** + * Observable abstraction over ngOnDestroy to use with takeUntil + * @example + *@Component({ + * selector: 'app-example', + * templateUrl: './example.component.html', + * styleUrls: [ './example.component.scss' ], + * providers: [ OnDestroyService ] <=== add to the element providers section + *}) + *export class ExampleComponent implements OnInit, OnDestroy { + * private stream$ = interval(200); + * + * constructor(@Self() private readonly destroy$: OnDestroyService) { <=== Inject, using @Self() + * } + * + * public ngOnInit(): void { + * this.stream$.pipe( + * map((i: number) => i * 2), + * takeUntil(this.destroy$) <=== Use in takeUntil() operator + * ).subscribe(); + * } + *} + */ +@Injectable() +export class OnDestroyService extends Subject implements OnDestroy { + public ngOnDestroy(): void { + this.next(null); + this.complete(); + } +} diff --git a/src/app/components/utils/outside-zone-operator.ts b/src/app/components/utils/outside-zone-operator.ts new file mode 100644 index 00000000000..03d117eb261 --- /dev/null +++ b/src/app/components/utils/outside-zone-operator.ts @@ -0,0 +1,21 @@ +import { NgZone } from '@angular/core'; +import { Observable } from 'rxjs'; + +/** + * RxJs operator, that run subscription function outside NgZone (This operator should be first in the pipe() method + * + * @param {NgZone} zone - injected ngZone reference + * + * @returns {Observable} Input Observable + * + * @example + * fromEvent(this.elementRef.nativeElement, 'click') + * .pipe( + * outsideZone(this.zone), + * ...other operators, + * takeUntil(this.destroy$) + * ) + */ +export function outsideZone(zone: NgZone): (source: Observable) => Observable { + return (source) => new Observable((subscriber) => zone.runOutsideAngular(() => source.subscribe(subscriber))); +}