Skip to content

Commit

Permalink
feat(tabs): add input to opt out of pagination
Browse files Browse the repository at this point in the history
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 angular#17317.
  • Loading branch information
crisbeto committed Oct 16, 2019
1 parent 43c7a7d commit 98d4e80
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 16 deletions.
38 changes: 37 additions & 1 deletion src/material-experimental/mdc-tabs/tab-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,40 @@ describe('MatTabHeader', () => {

});

describe('disabling pagination', () => {
it('should not show the pagination controls if pagination is disabled', () => {
fixture = TestBed.createComponent(SimpleTabHeaderApp);
appComponent = fixture.componentInstance;
appComponent.disablePagination = true;
fixture.detectChanges();
expect(appComponent.tabHeader._showPaginationControls).toBe(false);

// Add enough tabs that it will obviously exceed the width
appComponent.addTabsForScrolling();
fixture.detectChanges();

expect(appComponent.tabHeader._showPaginationControls).toBe(false);
});

it('should not change the scroll position if pagination is disabled', () => {
fixture = TestBed.createComponent(SimpleTabHeaderApp);
appComponent = fixture.componentInstance;
appComponent.disablePagination = true;
fixture.detectChanges();
appComponent.addTabsForScrolling();
fixture.detectChanges();
expect(appComponent.tabHeader.scrollDistance).toBe(0);

appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1;
fixture.detectChanges();
expect(appComponent.tabHeader.scrollDistance).toBe(0);

appComponent.tabHeader.focusIndex = 0;
fixture.detectChanges();
expect(appComponent.tabHeader.scrollDistance).toBe(0);
});
});

it('should re-align the ink bar when the direction changes', fakeAsync(() => {
fixture = TestBed.createComponent(SimpleTabHeaderApp);
fixture.detectChanges();
Expand Down Expand Up @@ -618,7 +652,8 @@ interface Tab {
<div [dir]="dir">
<mat-tab-header [selectedIndex]="selectedIndex" [disableRipple]="disableRipple"
(indexFocused)="focusedIndex = $event"
(selectFocusedIndex)="selectedIndex = $event">
(selectFocusedIndex)="selectedIndex = $event"
[disablePagination]="disablePagination">
<div matTabLabelWrapper class="label-content" style="min-width: 30px; width: 30px"
*ngFor="let tab of tabs; let i = index"
[disabled]="!!tab.disabled"
Expand All @@ -641,6 +676,7 @@ class SimpleTabHeaderApp {
disabledTabIndex = 1;
tabs: Tab[] = [{label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}];
dir: Direction = 'ltr';
disablePagination: boolean;

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

Expand Down
58 changes: 44 additions & 14 deletions src/material/tabs/paginated-tab-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
OnDestroy,
Directive,
Inject,
Input,
} from '@angular/core';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {coerceNumberProperty} from '@angular/cdk/coercion';
Expand Down Expand Up @@ -116,6 +117,13 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
/** Stream that will stop the automated scrolling. */
private _stopScrolling = new Subject<void>();

/**
* Whether pagination should be disabled. This can be used to avoid unnecessary
* layout recalculations if it's known that pagination won't be required.
*/
@Input()
disablePagination: boolean = false;

/** The index of the active tab. */
get selectedIndex(): number { return this._selectedIndex; }
set selectedIndex(value: number) {
Expand Down Expand Up @@ -364,6 +372,10 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte

/** Performs the CSS transformation on the tab list that will cause the list to scroll. */
_updateTabScrollPosition() {
if (this.disablePagination) {
return;
}

const scrollDistance = this.scrollDistance;
const platform = this._platform;
const translateX = this._getLayoutDirection() === 'ltr' ? -scrollDistance : scrollDistance;
Expand Down Expand Up @@ -422,9 +434,15 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
* should be called sparingly.
*/
_scrollToLabel(labelIndex: number) {
if (this.disablePagination) {
return;
}

const selectedLabel = this._items ? this._items.toArray()[labelIndex] : null;

if (!selectedLabel) { return; }
if (!selectedLabel) {
return;
}

// The view length is the visible width of the tab labels.
const viewLength = this._tabListContainer.nativeElement.offsetWidth;
Expand Down Expand Up @@ -460,18 +478,22 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
* should be called sparingly.
*/
_checkPaginationEnabled() {
const isEnabled =
this._tabList.nativeElement.scrollWidth > this._elementRef.nativeElement.offsetWidth;
if (this.disablePagination) {
this._showPaginationControls = false;
} else {
const isEnabled =
this._tabList.nativeElement.scrollWidth > this._elementRef.nativeElement.offsetWidth;

if (!isEnabled) {
this.scrollDistance = 0;
}
if (!isEnabled) {
this.scrollDistance = 0;
}

if (isEnabled !== this._showPaginationControls) {
this._changeDetectorRef.markForCheck();
}
if (isEnabled !== this._showPaginationControls) {
this._changeDetectorRef.markForCheck();
}

this._showPaginationControls = isEnabled;
this._showPaginationControls = isEnabled;
}
}

/**
Expand All @@ -484,10 +506,14 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
* should be called sparingly.
*/
_checkScrollingControls() {
// Check if the pagination arrows should be activated.
this._disableScrollBefore = this.scrollDistance == 0;
this._disableScrollAfter = this.scrollDistance == this._getMaxScrollDistance();
this._changeDetectorRef.markForCheck();
if (this.disablePagination) {
this._disableScrollAfter = this._disableScrollBefore = true;
} else {
// Check if the pagination arrows should be activated.
this._disableScrollBefore = this.scrollDistance == 0;
this._disableScrollAfter = this.scrollDistance == this._getMaxScrollDistance();
this._changeDetectorRef.markForCheck();
}
}

/**
Expand Down Expand Up @@ -550,6 +576,10 @@ export abstract class MatPaginatedTabHeader implements AfterContentChecked, Afte
* @returns Information on the current scroll distance and the maximum.
*/
private _scrollTo(position: number) {
if (this.disablePagination) {
return {maxScrollDistance: 0, distance: 0};
}

const maxScrollDistance = this._getMaxScrollDistance();
this._scrollDistance = Math.max(0, Math.min(maxScrollDistance, position));

Expand Down
1 change: 1 addition & 0 deletions src/material/tabs/tab-group.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<mat-tab-header #tabHeader
[selectedIndex]="selectedIndex"
[disableRipple]="disableRipple"
[disablePagination]="disablePagination"
(indexFocused)="_focusChanged($event)"
(selectFocusedIndex)="selectedIndex = $event">
<div class="mat-tab-label" role="tab" matTabLabelWrapper mat-ripple cdkMonitorElementFocus
Expand Down
15 changes: 15 additions & 0 deletions src/material/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export type MatTabHeaderPosition = 'above' | 'below';
export interface MatTabsConfig {
/** Duration for the tab animation. Must be a valid CSS value (e.g. 600ms). */
animationDuration?: string;

/**
* Whether pagination should be disabled. This can be used to avoid unnecessary
* layout recalculations if it's known that pagination won't be required.
*/
disablePagination?: boolean;
}

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

/**
* Whether pagination should be disabled. This can be used to avoid unnecessary
* layout recalculations if it's known that pagination won't be required.
*/
@Input()
disablePagination: boolean;

/** Background color of the tab group. */
@Input()
get backgroundColor(): ThemePalette { return this._backgroundColor; }
Expand Down Expand Up @@ -169,6 +182,8 @@ export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements
this._groupId = nextId++;
this.animationDuration = defaultConfig && defaultConfig.animationDuration ?
defaultConfig.animationDuration : '500ms';
this.disablePagination = defaultConfig && defaultConfig.disablePagination != null ?
defaultConfig.disablePagination : false;
}

/**
Expand Down
38 changes: 37 additions & 1 deletion src/material/tabs/tab-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,40 @@ describe('MatTabHeader', () => {

});

describe('disabling pagination', () => {
it('should not show the pagination controls if pagination is disabled', () => {
fixture = TestBed.createComponent(SimpleTabHeaderApp);
appComponent = fixture.componentInstance;
appComponent.disablePagination = true;
fixture.detectChanges();
expect(appComponent.tabHeader._showPaginationControls).toBe(false);

// Add enough tabs that it will obviously exceed the width
appComponent.addTabsForScrolling();
fixture.detectChanges();

expect(appComponent.tabHeader._showPaginationControls).toBe(false);
});

it('should not change the scroll position if pagination is disabled', () => {
fixture = TestBed.createComponent(SimpleTabHeaderApp);
appComponent = fixture.componentInstance;
appComponent.disablePagination = true;
fixture.detectChanges();
appComponent.addTabsForScrolling();
fixture.detectChanges();
expect(appComponent.tabHeader.scrollDistance).toBe(0);

appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1;
fixture.detectChanges();
expect(appComponent.tabHeader.scrollDistance).toBe(0);

appComponent.tabHeader.focusIndex = 0;
fixture.detectChanges();
expect(appComponent.tabHeader.scrollDistance).toBe(0);
});
});

it('should re-align the ink bar when the direction changes', fakeAsync(() => {
fixture = TestBed.createComponent(SimpleTabHeaderApp);

Expand Down Expand Up @@ -617,7 +651,8 @@ interface Tab {
<div [dir]="dir">
<mat-tab-header [selectedIndex]="selectedIndex" [disableRipple]="disableRipple"
(indexFocused)="focusedIndex = $event"
(selectFocusedIndex)="selectedIndex = $event">
(selectFocusedIndex)="selectedIndex = $event"
[disablePagination]="disablePagination">
<div matTabLabelWrapper class="label-content" style="min-width: 30px; width: 30px"
*ngFor="let tab of tabs; let i = index"
[disabled]="!!tab.disabled"
Expand All @@ -637,6 +672,7 @@ class SimpleTabHeaderApp {
disableRipple: boolean = false;
selectedIndex: number = 0;
focusedIndex: number;
disablePagination: boolean;
disabledTabIndex = 1;
tabs: Tab[] = [{label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}];
dir: Direction = 'ltr';
Expand Down
2 changes: 2 additions & 0 deletions tools/public_api_guard/material/tabs.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase imp
dynamicHeight: boolean;
readonly focusChange: EventEmitter<MatTabChangeEvent>;
headerPosition: MatTabHeaderPosition;
disablePagination: boolean;
selectedIndex: number | null;
readonly selectedIndexChange: EventEmitter<number>;
readonly selectedTabChange: EventEmitter<MatTabChangeEvent>;
Expand Down Expand Up @@ -191,6 +192,7 @@ export declare const matTabsAnimations: {

export interface MatTabsConfig {
animationDuration?: string;
disablePagination?: boolean;
}

export declare class MatTabsModule {
Expand Down

0 comments on commit 98d4e80

Please sign in to comment.