Skip to content

Commit 23ac073

Browse files
committed
feat(tabs): add input to opt out of pagination
Currently the tabs pagination works automatically by measuring the size of the tab header to figure out whether to show pagination. This measuring can be expensive because it triggers a page layout and might not necessarily be required if the page won't have enough tabs to paginate through. These changes add an input and an option to the injection token to allow consumers to opt out of the pagination, if they know that they won't need it. Fixes #17317.
1 parent 43c7a7d commit 23ac073

File tree

6 files changed

+136
-16
lines changed

6 files changed

+136
-16
lines changed

src/material-experimental/mdc-tabs/tab-header.spec.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,40 @@ describe('MatTabHeader', () => {
523523

524524
});
525525

526+
describe('disabling pagination', () => {
527+
it('should not show the pagination controls if pagination is disabled', () => {
528+
fixture = TestBed.createComponent(SimpleTabHeaderApp);
529+
appComponent = fixture.componentInstance;
530+
appComponent.disablePagination = true;
531+
fixture.detectChanges();
532+
expect(appComponent.tabHeader._showPaginationControls).toBe(false);
533+
534+
// Add enough tabs that it will obviously exceed the width
535+
appComponent.addTabsForScrolling();
536+
fixture.detectChanges();
537+
538+
expect(appComponent.tabHeader._showPaginationControls).toBe(false);
539+
});
540+
541+
it('should not change the scroll position if pagination is disabled', () => {
542+
fixture = TestBed.createComponent(SimpleTabHeaderApp);
543+
appComponent = fixture.componentInstance;
544+
appComponent.disablePagination = true;
545+
fixture.detectChanges();
546+
appComponent.addTabsForScrolling();
547+
fixture.detectChanges();
548+
expect(appComponent.tabHeader.scrollDistance).toBe(0);
549+
550+
appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1;
551+
fixture.detectChanges();
552+
expect(appComponent.tabHeader.scrollDistance).toBe(0);
553+
554+
appComponent.tabHeader.focusIndex = 0;
555+
fixture.detectChanges();
556+
expect(appComponent.tabHeader.scrollDistance).toBe(0);
557+
});
558+
});
559+
526560
it('should re-align the ink bar when the direction changes', fakeAsync(() => {
527561
fixture = TestBed.createComponent(SimpleTabHeaderApp);
528562
fixture.detectChanges();
@@ -618,7 +652,8 @@ interface Tab {
618652
<div [dir]="dir">
619653
<mat-tab-header [selectedIndex]="selectedIndex" [disableRipple]="disableRipple"
620654
(indexFocused)="focusedIndex = $event"
621-
(selectFocusedIndex)="selectedIndex = $event">
655+
(selectFocusedIndex)="selectedIndex = $event"
656+
[disablePagination]="disablePagination">
622657
<div matTabLabelWrapper class="label-content" style="min-width: 30px; width: 30px"
623658
*ngFor="let tab of tabs; let i = index"
624659
[disabled]="!!tab.disabled"
@@ -641,6 +676,7 @@ class SimpleTabHeaderApp {
641676
disabledTabIndex = 1;
642677
tabs: Tab[] = [{label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}];
643678
dir: Direction = 'ltr';
679+
disablePagination: boolean;
644680

645681
@ViewChild(MatTabHeader, {static: true}) tabHeader: MatTabHeader;
646682

src/material/tabs/paginated-tab-header.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
OnDestroy,
2020
Directive,
2121
Inject,
22+
Input,
2223
} from '@angular/core';
2324
import {Direction, Directionality} from '@angular/cdk/bidi';
2425
import {coerceNumberProperty} from '@angular/cdk/coercion';
@@ -116,6 +117,13 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
116117
/** Stream that will stop the automated scrolling. */
117118
private _stopScrolling = new Subject<void>();
118119

120+
/**
121+
* Whether pagination should be disabled. This can be used to avoid unnecessary
122+
* layout recalculations if it's known that pagination won't be required.
123+
*/
124+
@Input()
125+
disablePagination: boolean = false;
126+
119127
/** The index of the active tab. */
120128
get selectedIndex(): number { return this._selectedIndex; }
121129
set selectedIndex(value: number) {
@@ -364,6 +372,10 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
364372

365373
/** Performs the CSS transformation on the tab list that will cause the list to scroll. */
366374
_updateTabScrollPosition() {
375+
if (this.disablePagination) {
376+
return;
377+
}
378+
367379
const scrollDistance = this.scrollDistance;
368380
const platform = this._platform;
369381
const translateX = this._getLayoutDirection() === 'ltr' ? -scrollDistance : scrollDistance;
@@ -422,9 +434,15 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
422434
* should be called sparingly.
423435
*/
424436
_scrollToLabel(labelIndex: number) {
437+
if (this.disablePagination) {
438+
return;
439+
}
440+
425441
const selectedLabel = this._items ? this._items.toArray()[labelIndex] : null;
426442

427-
if (!selectedLabel) { return; }
443+
if (!selectedLabel) {
444+
return;
445+
}
428446

429447
// The view length is the visible width of the tab labels.
430448
const viewLength = this._tabListContainer.nativeElement.offsetWidth;
@@ -460,18 +478,22 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
460478
* should be called sparingly.
461479
*/
462480
_checkPaginationEnabled() {
463-
const isEnabled =
464-
this._tabList.nativeElement.scrollWidth > this._elementRef.nativeElement.offsetWidth;
481+
if (this.disablePagination) {
482+
this._showPaginationControls = false;
483+
} else {
484+
const isEnabled =
485+
this._tabList.nativeElement.scrollWidth > this._elementRef.nativeElement.offsetWidth;
465486

466-
if (!isEnabled) {
467-
this.scrollDistance = 0;
468-
}
487+
if (!isEnabled) {
488+
this.scrollDistance = 0;
489+
}
469490

470-
if (isEnabled !== this._showPaginationControls) {
471-
this._changeDetectorRef.markForCheck();
472-
}
491+
if (isEnabled !== this._showPaginationControls) {
492+
this._changeDetectorRef.markForCheck();
493+
}
473494

474-
this._showPaginationControls = isEnabled;
495+
this._showPaginationControls = isEnabled;
496+
}
475497
}
476498

477499
/**
@@ -484,10 +506,14 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
484506
* should be called sparingly.
485507
*/
486508
_checkScrollingControls() {
487-
// Check if the pagination arrows should be activated.
488-
this._disableScrollBefore = this.scrollDistance == 0;
489-
this._disableScrollAfter = this.scrollDistance == this._getMaxScrollDistance();
490-
this._changeDetectorRef.markForCheck();
509+
if (this.disablePagination) {
510+
this._disableScrollAfter = this._disableScrollBefore = true;
511+
} else {
512+
// Check if the pagination arrows should be activated.
513+
this._disableScrollBefore = this.scrollDistance == 0;
514+
this._disableScrollAfter = this.scrollDistance == this._getMaxScrollDistance();
515+
this._changeDetectorRef.markForCheck();
516+
}
491517
}
492518

493519
/**
@@ -550,6 +576,10 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
550576
* @returns Information on the current scroll distance and the maximum.
551577
*/
552578
private _scrollTo(position: number) {
579+
if (this.disablePagination) {
580+
return {maxScrollDistance: 0, distance: 0};
581+
}
582+
553583
const maxScrollDistance = this._getMaxScrollDistance();
554584
this._scrollDistance = Math.max(0, Math.min(maxScrollDistance, position));
555585

src/material/tabs/tab-group.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<mat-tab-header #tabHeader
22
[selectedIndex]="selectedIndex"
33
[disableRipple]="disableRipple"
4+
[disablePagination]="disablePagination"
45
(indexFocused)="_focusChanged($event)"
56
(selectFocusedIndex)="selectedIndex = $event">
67
<div class="mat-tab-label" role="tab" matTabLabelWrapper mat-ripple cdkMonitorElementFocus

src/material/tabs/tab-group.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ export type MatTabHeaderPosition = 'above' | 'below';
5959
export interface MatTabsConfig {
6060
/** Duration for the tab animation. Must be a valid CSS value (e.g. 600ms). */
6161
animationDuration?: string;
62+
63+
/**
64+
* Whether pagination should be disabled. This can be used to avoid unnecessary
65+
* layout recalculations if it's known that pagination won't be required.
66+
*/
67+
disablePagination?: boolean;
6268
}
6369

6470
/** Injection token that can be used to provide the default options the tabs module. */
@@ -129,6 +135,13 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
129135
}
130136
private _animationDuration: string;
131137

138+
/**
139+
* Whether pagination should be disabled. This can be used to avoid unnecessary
140+
* layout recalculations if it's known that pagination won't be required.
141+
*/
142+
@Input()
143+
disablePagination: boolean;
144+
132145
/** Background color of the tab group. */
133146
@Input()
134147
get backgroundColor(): ThemePalette { return this._backgroundColor; }
@@ -169,6 +182,8 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
169182
this._groupId = nextId++;
170183
this.animationDuration = defaultConfig && defaultConfig.animationDuration ?
171184
defaultConfig.animationDuration : '500ms';
185+
this.disablePagination = defaultConfig && defaultConfig.disablePagination != null ?
186+
defaultConfig.disablePagination : false;
172187
}
173188

174189
/**

src/material/tabs/tab-header.spec.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,40 @@ describe('MatTabHeader', () => {
523523

524524
});
525525

526+
describe('disabling pagination', () => {
527+
it('should not show the pagination controls if pagination is disabled', () => {
528+
fixture = TestBed.createComponent(SimpleTabHeaderApp);
529+
appComponent = fixture.componentInstance;
530+
appComponent.disablePagination = true;
531+
fixture.detectChanges();
532+
expect(appComponent.tabHeader._showPaginationControls).toBe(false);
533+
534+
// Add enough tabs that it will obviously exceed the width
535+
appComponent.addTabsForScrolling();
536+
fixture.detectChanges();
537+
538+
expect(appComponent.tabHeader._showPaginationControls).toBe(false);
539+
});
540+
541+
it('should not change the scroll position if pagination is disabled', () => {
542+
fixture = TestBed.createComponent(SimpleTabHeaderApp);
543+
appComponent = fixture.componentInstance;
544+
appComponent.disablePagination = true;
545+
fixture.detectChanges();
546+
appComponent.addTabsForScrolling();
547+
fixture.detectChanges();
548+
expect(appComponent.tabHeader.scrollDistance).toBe(0);
549+
550+
appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1;
551+
fixture.detectChanges();
552+
expect(appComponent.tabHeader.scrollDistance).toBe(0);
553+
554+
appComponent.tabHeader.focusIndex = 0;
555+
fixture.detectChanges();
556+
expect(appComponent.tabHeader.scrollDistance).toBe(0);
557+
});
558+
});
559+
526560
it('should re-align the ink bar when the direction changes', fakeAsync(() => {
527561
fixture = TestBed.createComponent(SimpleTabHeaderApp);
528562

@@ -617,7 +651,8 @@ interface Tab {
617651
<div [dir]="dir">
618652
<mat-tab-header [selectedIndex]="selectedIndex" [disableRipple]="disableRipple"
619653
(indexFocused)="focusedIndex = $event"
620-
(selectFocusedIndex)="selectedIndex = $event">
654+
(selectFocusedIndex)="selectedIndex = $event"
655+
[disablePagination]="disablePagination">
621656
<div matTabLabelWrapper class="label-content" style="min-width: 30px; width: 30px"
622657
*ngFor="let tab of tabs; let i = index"
623658
[disabled]="!!tab.disabled"
@@ -637,6 +672,7 @@ class SimpleTabHeaderApp {
637672
disableRipple: boolean = false;
638673
selectedIndex: number = 0;
639674
focusedIndex: number;
675+
disablePagination: boolean;
640676
disabledTabIndex = 1;
641677
tabs: Tab[] = [{label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}];
642678
dir: Direction = 'ltr';

tools/public_api_guard/material/tabs.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase imp
3535
readonly animationDone: EventEmitter<void>;
3636
animationDuration: string;
3737
backgroundColor: ThemePalette;
38+
disablePagination: boolean;
3839
dynamicHeight: boolean;
3940
readonly focusChange: EventEmitter<MatTabChangeEvent>;
4041
headerPosition: MatTabHeaderPosition;
@@ -191,6 +192,7 @@ export declare const matTabsAnimations: {
191192

192193
export interface MatTabsConfig {
193194
animationDuration?: string;
195+
disablePagination?: boolean;
194196
}
195197

196198
export declare class MatTabsModule {

0 commit comments

Comments
 (0)