diff --git a/projects/addon-charts/components/arc-chart/arc-chart.component.ts b/projects/addon-charts/components/arc-chart/arc-chart.component.ts index a1407d237d7c..0c9765d098d1 100644 --- a/projects/addon-charts/components/arc-chart/arc-chart.component.ts +++ b/projects/addon-charts/components/arc-chart/arc-chart.component.ts @@ -8,13 +8,14 @@ import { Input, Output, QueryList, + Self, ViewChildren, } from '@angular/core'; import {DomSanitizer, SafeValue} from '@angular/platform-browser'; -import {tuiTypedFromEvent} from '@taiga-ui/cdk'; +import {TuiDestroyService, tuiTypedFromEvent, tuiWatch} from '@taiga-ui/cdk'; import {TuiSizeXL} from '@taiga-ui/core'; -import {merge, Observable, ReplaySubject} from 'rxjs'; -import {map, startWith, switchMap, tap} from 'rxjs/operators'; +import {merge, Observable, ReplaySubject, timer} from 'rxjs'; +import {map, startWith, switchMap, takeUntil, tap} from 'rxjs/operators'; // 3/4 with 1% safety offset const ARC = 0.76; @@ -51,6 +52,7 @@ function arcsToIndex(arcs: QueryList>): Array>>(1); @@ -97,12 +99,13 @@ export class TuiArcChartComponent { constructor( @Inject(DomSanitizer) private readonly sanitizer: DomSanitizer, @Inject(ChangeDetectorRef) cdr: ChangeDetectorRef, + @Self() @Inject(TuiDestroyService) destroy$: Observable, ) { - // So initial animation works - setTimeout(() => { - this.initialized = true; - cdr.markForCheck(); - }); + timer(0) + .pipe(tuiWatch(cdr), takeUntil(destroy$)) + .subscribe(() => { + this.initialized = true; + }); } @HostBinding('style.width.rem') diff --git a/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts b/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts index ca8f23e3fbd2..526b334ac0ee 100644 --- a/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts +++ b/projects/addon-mobile/components/mobile-calendar/mobile-calendar.component.ts @@ -27,6 +27,7 @@ import { TuiMonth, tuiTypedFromEvent, TuiTypedMapper, + tuiZonefree, } from '@taiga-ui/cdk'; import { TUI_ANIMATIONS_DURATION, @@ -221,10 +222,10 @@ export class TuiMobileCalendarComponent implements AfterViewInit { this.activeYear = year; this.scrollToActiveYear('smooth'); - this.ngZone.runOutsideAngular(() => { - // Delay is required to run months scroll in the next frame to prevent flicker - setTimeout(() => this.scrollToActiveMonth()); - }); + // Delay is required to run months scroll in the next frame to prevent flicker + timer(0) + .pipe(tuiZonefree(this.ngZone), takeUntil(this.destroy$)) + .subscribe(() => this.scrollToActiveMonth()); } readonly disabledItemHandlerMapper: TuiTypedMapper< diff --git a/projects/addon-mobile/components/sheet/components/sheet/sheet.component.ts b/projects/addon-mobile/components/sheet/components/sheet/sheet.component.ts index b754a5e7c368..9ddb581ba592 100644 --- a/projects/addon-mobile/components/sheet/components/sheet/sheet.component.ts +++ b/projects/addon-mobile/components/sheet/components/sheet/sheet.component.ts @@ -8,16 +8,22 @@ import { Input, NgZone, QueryList, + Self, ViewChild, ViewChildren, } from '@angular/core'; -import {EMPTY_QUERY, TUI_IS_IOS, tuiPure, tuiZonefull} from '@taiga-ui/cdk'; +import { + EMPTY_QUERY, + TUI_IS_IOS, + TuiDestroyService, + tuiPure, + tuiZonefull, +} from '@taiga-ui/cdk'; import {tuiSlideInTop} from '@taiga-ui/core'; import {TUI_MORE_WORD} from '@taiga-ui/kit'; -import {Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; +import {Observable, timer} from 'rxjs'; +import {map, takeUntil} from 'rxjs/operators'; -import {fakeSmoothScroll} from '../../ios.hacks'; import {TuiSheet, TuiSheetRequiredProps} from '../../sheet'; import {TUI_SHEET_SCROLL} from '../../sheet-tokens'; import {TUI_SHEET_ID} from '../sheet-heading/sheet-heading.component'; @@ -62,6 +68,7 @@ export class TuiSheetComponent implements TuiSheetRequiredProps, AfterView @Inject(NgZone) private readonly zone: NgZone, @Inject(TUI_IS_IOS) readonly isIos: boolean, @Inject(TUI_MORE_WORD) readonly moreWord$: Observable, + @Self() @Inject(TuiDestroyService) private readonly destroy$: Observable, ) {} get stops(): readonly number[] { @@ -99,7 +106,17 @@ export class TuiSheetComponent implements TuiSheetRequiredProps, AfterView const {nativeElement} = this.el; if (this.isIos) { - fakeSmoothScroll(nativeElement, top - nativeElement.scrollTop - 16); + const offset = top - nativeElement.scrollTop - 16; + + nativeElement.style.transition = 'none'; + nativeElement.style.transform = `scaleX(-1) translate3d(0, ${offset}px, 0)`; + + timer(0) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + nativeElement.style.transition = ''; + nativeElement.style.transform = ''; + }); } nativeElement.scrollTo({top, behavior: 'smooth'}); diff --git a/projects/addon-mobile/components/sheet/directives/sheet-stop/sheet-stop.directive.ts b/projects/addon-mobile/components/sheet/directives/sheet-stop/sheet-stop.directive.ts index 67f7432505a8..b52f4981ad12 100644 --- a/projects/addon-mobile/components/sheet/directives/sheet-stop/sheet-stop.directive.ts +++ b/projects/addon-mobile/components/sheet/directives/sheet-stop/sheet-stop.directive.ts @@ -1,6 +1,6 @@ import {Directive, ElementRef, Inject, Self} from '@angular/core'; import {TUI_SCROLL_REF, TuiDestroyService} from '@taiga-ui/cdk'; -import {Observable} from 'rxjs'; +import {Observable, timer} from 'rxjs'; import { distinctUntilChanged, filter, @@ -39,9 +39,12 @@ export class TuiSheetStopDirective { nativeElement.classList.remove('_stuck'); // iOS nativeElement.scrollTop = el.nativeElement.offsetTop; - setTimeout(() => { - nativeElement.style.overflow = ''; - }, 100); + timer(100) + .pipe(takeUntil(destroy$)) + // eslint-disable-next-line rxjs/no-nested-subscribe + .subscribe(() => { + nativeElement.style.overflow = ''; + }); }); } } diff --git a/projects/addon-mobile/components/sheet/ios.hacks.ts b/projects/addon-mobile/components/sheet/ios.hacks.ts index 38b882e43e8b..9b59d3e4e86f 100644 --- a/projects/addon-mobile/components/sheet/ios.hacks.ts +++ b/projects/addon-mobile/components/sheet/ios.hacks.ts @@ -40,14 +40,3 @@ export function iosScrollFactory( return concat(scroll$.pipe(take(1)), result$).pipe(tuiZonefree(zone), share()); } - -// eslint-disable-next-line @typescript-eslint/naming-convention -export function fakeSmoothScroll({style}: HTMLElement, offset: number): void { - style.transition = 'none'; - style.transform = `scaleX(-1) translate3d(0, ${offset}px, 0)`; - - setTimeout(() => { - style.transition = ''; - style.transform = ''; - }); -} diff --git a/projects/demo/src/modules/components/accordion/accordion.component.ts b/projects/demo/src/modules/components/accordion/accordion.component.ts index 21530ccdc26c..af3459345da0 100644 --- a/projects/demo/src/modules/components/accordion/accordion.component.ts +++ b/projects/demo/src/modules/components/accordion/accordion.component.ts @@ -1,12 +1,16 @@ -import {Component, ElementRef, ViewChild} from '@angular/core'; +import {Component, ElementRef, Inject, Self, ViewChild} from '@angular/core'; import {changeDetection} from '@demo/emulate/change-detection'; import {TuiDocExample} from '@taiga-ui/addon-doc'; +import {TuiDestroyService} from '@taiga-ui/cdk'; import {TUI_EXPAND_LOADED, TuiSizeS} from '@taiga-ui/core'; +import {Observable, timer} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; @Component({ selector: 'example-accordion', templateUrl: './accordion.template.html', changeDetection, + providers: [TuiDestroyService], }) export class ExampleTuiAccordionComponent { @ViewChild('content') @@ -61,6 +65,10 @@ export class ExampleTuiAccordionComponent { size: TuiSizeS = this.sizeVariants[1]; + constructor( + @Self() @Inject(TuiDestroyService) private readonly destroy$: Observable, + ) {} + onOpenChange(open: boolean): void { this.open = open; @@ -68,12 +76,13 @@ export class ExampleTuiAccordionComponent { return; } - setTimeout(() => { - const event = new CustomEvent(TUI_EXPAND_LOADED, {bubbles: true}); - - if (this.content) { - this.content.nativeElement.dispatchEvent(event); - } - }, 3000); + timer(3000) + .pipe(takeUntil(this.destroy$)) + .subscribe( + () => + this.content?.nativeElement.dispatchEvent( + new CustomEvent(TUI_EXPAND_LOADED, {bubbles: true}), + ), + ); } } diff --git a/projects/demo/src/modules/components/expand/expand.component.ts b/projects/demo/src/modules/components/expand/expand.component.ts index e1ac53037476..b243046c215a 100644 --- a/projects/demo/src/modules/components/expand/expand.component.ts +++ b/projects/demo/src/modules/components/expand/expand.component.ts @@ -1,13 +1,24 @@ -import {ChangeDetectorRef, Component, ElementRef, Inject, ViewChild} from '@angular/core'; +import { + ChangeDetectorRef, + Component, + ElementRef, + Inject, + Self, + ViewChild, +} from '@angular/core'; import {changeDetection} from '@demo/emulate/change-detection'; import {TuiDocExample} from '@taiga-ui/addon-doc'; +import {TuiDestroyService} from '@taiga-ui/cdk'; import {TUI_EXPAND_LOADED, TuiExpandComponent} from '@taiga-ui/core'; +import {Observable, timer} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; @Component({ selector: 'example-expand', templateUrl: './expand.template.html', styleUrls: ['./expand.style.less'], changeDetection, + providers: [TuiDestroyService], }) export class ExampleTuiExpandComponent { @ViewChild(TuiExpandComponent, {read: ElementRef}) @@ -27,7 +38,10 @@ export class ExampleTuiExpandComponent { delayed = false; - constructor(@Inject(ChangeDetectorRef) private readonly cdr: ChangeDetectorRef) {} + constructor( + @Inject(ChangeDetectorRef) private readonly cdr: ChangeDetectorRef, + @Self() @Inject(TuiDestroyService) private readonly destroy$: Observable, + ) {} onExpandedChange(expanded: boolean): void { this.expanded = expanded; @@ -37,15 +51,14 @@ export class ExampleTuiExpandComponent { return; } - setTimeout(() => { - const event = new CustomEvent(TUI_EXPAND_LOADED, {bubbles: true}); + timer(5000) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + const event = new CustomEvent(TUI_EXPAND_LOADED, {bubbles: true}); - this.delayed = false; - this.cdr.detectChanges(); - - if (this.expand) { - this.expand.nativeElement.dispatchEvent(event); - } - }, 5000); + this.delayed = false; + this.cdr.detectChanges(); + this.expand?.nativeElement.dispatchEvent(event); + }); } } diff --git a/projects/demo/src/modules/directives/for/examples/1/index.ts b/projects/demo/src/modules/directives/for/examples/1/index.ts index 17f99170c16d..c1af729553e9 100644 --- a/projects/demo/src/modules/directives/for/examples/1/index.ts +++ b/projects/demo/src/modules/directives/for/examples/1/index.ts @@ -1,28 +1,37 @@ -import {Component} from '@angular/core'; +import {Component, Inject, Self} from '@angular/core'; import {changeDetection} from '@demo/emulate/change-detection'; import {encapsulation} from '@demo/emulate/encapsulation'; -import {BehaviorSubject} from 'rxjs'; +import {TuiDestroyService} from '@taiga-ui/cdk'; +import {BehaviorSubject, Observable, timer} from 'rxjs'; +import {takeUntil} from 'rxjs/operators'; @Component({ selector: 'tui-for-example-1', templateUrl: './index.html', encapsulation, changeDetection, + providers: [TuiDestroyService], }) export class TuiForExample1 { readonly items$ = new BehaviorSubject([]); + constructor( + @Self() @Inject(TuiDestroyService) private readonly destroy$: Observable, + ) {} + refresh(): void { this.items$.next(null); const delay = Math.round(Math.random() * 2000); - setTimeout(() => { - this.items$.next( - delay % 2 - ? [] - : ['William "Bill" S. Preston Esq.', 'Ted "Theodore" Logan'], + timer(delay) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => + this.items$.next( + delay % 2 + ? [] + : ['William "Bill" S. Preston Esq.', 'Ted "Theodore" Logan'], + ), ); - }, delay); } } diff --git a/projects/kit/components/input-date-time/input-date-time.component.ts b/projects/kit/components/input-date-time/input-date-time.component.ts index 5bcb6ce1acba..0fca58ee17be 100644 --- a/projects/kit/components/input-date-time/input-date-time.component.ts +++ b/projects/kit/components/input-date-time/input-date-time.component.ts @@ -59,8 +59,8 @@ import { tuiDateStreamWithTransformer, TuiInputDateOptions, } from '@taiga-ui/kit/tokens'; -import {combineLatest, Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; +import {combineLatest, Observable, timer} from 'rxjs'; +import {map, takeUntil} from 'rxjs/operators'; @Component({ selector: 'tui-input-date-time', @@ -291,9 +291,11 @@ export class TuiInputDateTimeComponent return; } - setTimeout(() => { - this.nativeValue = this.trimTrailingSeparator(this.nativeValue); - }); + timer(0) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.nativeValue = this.trimTrailingSeparator(this.nativeValue); + }); if ( this.value[0] === null || diff --git a/projects/kit/components/input-tag/input-tag.component.ts b/projects/kit/components/input-tag/input-tag.component.ts index 1085f3001df5..66f18a34ebb7 100644 --- a/projects/kit/components/input-tag/input-tag.component.ts +++ b/projects/kit/components/input-tag/input-tag.component.ts @@ -59,8 +59,8 @@ import {TuiStringifiableItem} from '@taiga-ui/kit/classes'; import {FIXED_DROPDOWN_CONTROLLER_PROVIDER} from '@taiga-ui/kit/providers'; import {TuiStatus} from '@taiga-ui/kit/types'; import {PolymorpheusContent} from '@tinkoff/ng-polymorpheus'; -import {Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; +import {Observable, timer} from 'rxjs'; +import {map, takeUntil} from 'rxjs/operators'; import {TUI_INPUT_TAG_OPTIONS, TuiInputTagOptions} from './input-tag.options'; @@ -459,11 +459,13 @@ export class TuiInputTagComponent private scrollTo(scrollLeft = this.scrollBar?.nativeElement.scrollWidth): void { // Allow change detection to run and add new tag to DOM - setTimeout(() => { - if (this.scrollBar) { - this.scrollBar.nativeElement.scrollLeft = scrollLeft || 0; - } - }); + timer(0) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + if (this.scrollBar) { + this.scrollBar.nativeElement.scrollLeft = scrollLeft || 0; + } + }); } private filterValue(value: string[]): string[] { diff --git a/projects/kit/components/input-time/input-time.component.ts b/projects/kit/components/input-time/input-time.component.ts index b8498d195998..3c48e59ba1d1 100644 --- a/projects/kit/components/input-time/input-time.component.ts +++ b/projects/kit/components/input-time/input-time.component.ts @@ -44,8 +44,8 @@ import { import {TUI_SELECT_OPTION} from '@taiga-ui/kit/components/select-option'; import {FIXED_DROPDOWN_CONTROLLER_PROVIDER} from '@taiga-ui/kit/providers'; import {TUI_TIME_TEXTS} from '@taiga-ui/kit/tokens'; -import {Observable} from 'rxjs'; -import {map} from 'rxjs/operators'; +import {Observable, timer} from 'rxjs'; +import {map, takeUntil} from 'rxjs/operators'; import {TUI_INPUT_TIME_OPTIONS, TuiInputTimeOptions} from './input-time.options'; @@ -230,11 +230,13 @@ export class TuiInputTimeComponent this.value = TuiTime.fromString(this.nativeValue); - setTimeout(() => { - if (this.nativeValue.endsWith('.') || this.nativeValue.endsWith(':')) { - this.nativeValue = this.nativeValue.slice(0, -1); - } - }); + timer(0) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + if (this.nativeValue.endsWith('.') || this.nativeValue.endsWith(':')) { + this.nativeValue = this.nativeValue.slice(0, -1); + } + }); } onArrowUp(event: Event): void {