Skip to content

feat(tabs): add input to opt out of pagination #17409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 17, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -35,6 +35,7 @@ export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase imp
readonly animationDone: EventEmitter<void>;
animationDuration: string;
backgroundColor: ThemePalette;
disablePagination: boolean;
dynamicHeight: boolean;
readonly focusChange: EventEmitter<MatTabChangeEvent>;
headerPosition: MatTabHeaderPosition;
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