66 * found in the LICENSE file at https://angular.io/license
77 */
88
9+ import { FocusKeyManager , FocusableOption } from '@angular/cdk/a11y' ;
10+ import { Direction , Directionality } from '@angular/cdk/bidi' ;
11+ import { ENTER , SPACE , hasModifierKey } from '@angular/cdk/keycodes' ;
12+ import { SharedResizeObserver } from '@angular/cdk/observers/private' ;
13+ import { Platform , normalizePassiveListenerOptions } from '@angular/cdk/platform' ;
14+ import { ViewportRuler } from '@angular/cdk/scrolling' ;
915import {
10- ChangeDetectorRef ,
11- ElementRef ,
12- NgZone ,
13- Optional ,
14- QueryList ,
15- EventEmitter ,
16+ ANIMATION_MODULE_TYPE ,
1617 AfterContentChecked ,
1718 AfterContentInit ,
1819 AfterViewInit ,
19- OnDestroy ,
20+ ChangeDetectorRef ,
2021 Directive ,
22+ ElementRef ,
23+ EventEmitter ,
2124 Inject ,
25+ Injector ,
2226 Input ,
27+ NgZone ,
28+ OnDestroy ,
29+ Optional ,
30+ Output ,
31+ QueryList ,
32+ afterNextRender ,
2333 booleanAttribute ,
34+ inject ,
2435 numberAttribute ,
25- Output ,
26- ANIMATION_MODULE_TYPE ,
2736} from '@angular/core' ;
28- import { Direction , Directionality } from '@angular/cdk/bidi' ;
29- import { ViewportRuler } from '@angular/cdk/scrolling' ;
30- import { FocusKeyManager , FocusableOption } from '@angular/cdk/a11y' ;
31- import { ENTER , SPACE , hasModifierKey } from '@angular/cdk/keycodes' ;
3237import {
33- merge ,
34- of as observableOf ,
35- Subject ,
3638 EMPTY ,
37- Observer ,
3839 Observable ,
39- timer ,
40+ Observer ,
41+ Subject ,
4042 fromEvent ,
43+ merge ,
44+ of as observableOf ,
45+ timer ,
4146} from 'rxjs' ;
42- import { take , switchMap , startWith , skip , takeUntil , filter } from 'rxjs/operators' ;
43- import { Platform , normalizePassiveListenerOptions } from '@angular/cdk/platform' ;
47+ import { debounceTime , filter , skip , startWith , switchMap , takeUntil } from 'rxjs/operators' ;
4448
4549/** Config used to bind passive event listeners */
4650const passiveEventListenerOptions = normalizePassiveListenerOptions ( {
@@ -153,6 +157,10 @@ export abstract class MatPaginatedTabHeader
153157 /** Event emitted when a label is focused. */
154158 @Output ( ) readonly indexFocused : EventEmitter < number > = new EventEmitter < number > ( ) ;
155159
160+ private _sharedResizeObserver = inject ( SharedResizeObserver ) ;
161+
162+ private _injector = inject ( Injector ) ;
163+
156164 constructor (
157165 protected _elementRef : ElementRef < HTMLElement > ,
158166 protected _changeDetectorRef : ChangeDetectorRef ,
@@ -192,7 +200,18 @@ export abstract class MatPaginatedTabHeader
192200
193201 ngAfterContentInit ( ) {
194202 const dirChange = this . _dir ? this . _dir . change : observableOf ( 'ltr' ) ;
195- const resize = this . _viewportRuler . change ( 150 ) ;
203+ // We need to debounce resize events because the alignment logic is expensive.
204+ // If someone animates the width of tabs, we don't want to realign on every animation frame.
205+ // Once we haven't seen any more resize events in the last 32ms (~2 animaion frames) we can
206+ // re-align.
207+ const resize = this . _sharedResizeObserver
208+ . observe ( this . _elementRef . nativeElement )
209+ . pipe ( debounceTime ( 32 ) , takeUntil ( this . _destroyed ) ) ;
210+ // Note: We do not actually need to watch these events for proper functioning of the tabs,
211+ // the resize events above should capture any viewport resize that we care about. However,
212+ // removing this is fairly breaking for screenshot tests, so we're leaving it here for now.
213+ const viewportResize = this . _viewportRuler . change ( 150 ) . pipe ( takeUntil ( this . _destroyed ) ) ;
214+
196215 const realign = ( ) => {
197216 this . updatePagination ( ) ;
198217 this . _alignInkBarToSelectedTab ( ) ;
@@ -207,15 +226,14 @@ export abstract class MatPaginatedTabHeader
207226
208227 this . _keyManager . updateActiveItem ( this . _selectedIndex ) ;
209228
210- // Defer the first call in order to allow for slower browsers to lay out the elements.
211- // This helps in cases where the user lands directly on a page with paginated tabs.
212- // Note that we use `onStable` instead of `requestAnimationFrame`, because the latter
213- // can hold up tests that are in a background tab.
214- this . _ngZone . onStable . pipe ( take ( 1 ) ) . subscribe ( realign ) ;
229+ // Note: We do not need to realign after the first render for proper functioning of the tabs
230+ // the resize events above should fire when we first start observing the element. However,
231+ // removing this is fairly breaking for screenshot tests, so we're leaving it here for now.
232+ afterNextRender ( realign , { injector : this . _injector } ) ;
215233
216- // On dir change or window resize, realign the ink bar and update the orientation of
234+ // On dir change or resize, realign the ink bar and update the orientation of
217235 // the key manager if the direction has changed.
218- merge ( dirChange , resize , this . _items . changes , this . _itemsResized ( ) )
236+ merge ( dirChange , viewportResize , resize , this . _items . changes , this . _itemsResized ( ) )
219237 . pipe ( takeUntil ( this . _destroyed ) )
220238 . subscribe ( ( ) => {
221239 // We need to defer this to give the browser some time to recalculate
0 commit comments