Skip to content

Commit bb9a3a8

Browse files
crisbetommalerba
authored andcommitted
feat(tabs): add input to opt out of pagination (#17409)
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 71ec6e9 commit bb9a3a8

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
@@ -60,6 +60,12 @@ export type MatTabHeaderPosition = 'above' | 'below';
6060
export interface MatTabsConfig {
6161
/** Duration for the tab animation. Must be a valid CSS value (e.g. 600ms). */
6262
animationDuration?: string;
63+
64+
/**
65+
* Whether pagination should be disabled. This can be used to avoid unnecessary
66+
* layout recalculations if it's known that pagination won't be required.
67+
*/
68+
disablePagination?: boolean;
6369
}
6470

6571
/** Injection token that can be used to provide the default options the tabs module. */
@@ -138,6 +144,13 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
138144
}
139145
private _animationDuration: string;
140146

147+
/**
148+
* Whether pagination should be disabled. This can be used to avoid unnecessary
149+
* layout recalculations if it's known that pagination won't be required.
150+
*/
151+
@Input()
152+
disablePagination: boolean;
153+
141154
/** Background color of the tab group. */
142155
@Input()
143156
get backgroundColor(): ThemePalette { return this._backgroundColor; }
@@ -178,6 +191,8 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
178191
this._groupId = nextId++;
179192
this.animationDuration = defaultConfig && defaultConfig.animationDuration ?
180193
defaultConfig.animationDuration : '500ms';
194+
this.disablePagination = defaultConfig && defaultConfig.disablePagination != null ?
195+
defaultConfig.disablePagination : false;
181196
}
182197

183198
/**

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
@@ -36,6 +36,7 @@ export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase imp
3636
readonly animationDone: EventEmitter<void>;
3737
animationDuration: string;
3838
backgroundColor: ThemePalette;
39+
disablePagination: boolean;
3940
dynamicHeight: boolean;
4041
readonly focusChange: EventEmitter<MatTabChangeEvent>;
4142
headerPosition: MatTabHeaderPosition;
@@ -196,6 +197,7 @@ export declare const matTabsAnimations: {
196197

197198
export interface MatTabsConfig {
198199
animationDuration?: string;
200+
disablePagination?: boolean;
199201
}
200202

201203
export declare class MatTabsModule {

0 commit comments

Comments
 (0)