+ `
+})
+class TabGroupWithIsActiveBinding {
+}
+
+
+@Component({
+ template: `
+
+ Tab one content
+ Tab two content
+
+ `,
+})
+class TabsWithCustomAnimationDuration {}
diff --git a/src/material-experimental/mdc-tabs/tab-group.ts b/src/material-experimental/mdc-tabs/tab-group.ts
new file mode 100644
index 000000000000..0d958dee9e6d
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-group.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ ChangeDetectionStrategy,
+ Component,
+ ContentChildren,
+ ElementRef,
+ QueryList,
+ ViewChild,
+ ViewEncapsulation,
+ ChangeDetectorRef,
+ Inject,
+ Optional,
+} from '@angular/core';
+import {_MatTabGroupBase, MAT_TABS_CONFIG, MatTabsConfig} from '@angular/material/tabs';
+import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
+import {MatTab} from './tab';
+import {MatTabHeader} from './tab-header';
+
+/**
+ * Material design tab-group component. Supports basic tab pairs (label + content) and includes
+ * animated ink-bar, keyboard navigation, and screen reader.
+ * See: https://material.io/design/components/tabs.html
+ */
+@Component({
+ moduleId: module.id,
+ selector: 'mat-tab-group',
+ exportAs: 'matTabGroup',
+ templateUrl: 'tab-group.html',
+ styleUrls: ['tab-group.css'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ inputs: ['color', 'disableRipple'],
+ host: {
+ 'class': 'mat-mdc-tab-group',
+ '[class.mat-mdc-tab-group-dynamic-height]': 'dynamicHeight',
+ '[class.mat-mdc-tab-group-inverted-header]': 'headerPosition === "below"',
+ },
+})
+export class MatTabGroup extends _MatTabGroupBase {
+ @ContentChildren(MatTab) _tabs: QueryList;
+ @ViewChild('tabBodyWrapper', {static: false}) _tabBodyWrapper: ElementRef;
+ @ViewChild('tabHeader', {static: false}) _tabHeader: MatTabHeader;
+
+ constructor(elementRef: ElementRef,
+ changeDetectorRef: ChangeDetectorRef,
+ @Inject(MAT_TABS_CONFIG) @Optional() defaultConfig?: MatTabsConfig,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
+ super(elementRef, changeDetectorRef, defaultConfig, animationMode);
+ }
+}
diff --git a/src/material-experimental/mdc-tabs/tab-header.html b/src/material-experimental/mdc-tabs/tab-header.html
new file mode 100644
index 000000000000..115739bee519
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-header.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/material-experimental/mdc-tabs/tab-header.scss b/src/material-experimental/mdc-tabs/tab-header.scss
new file mode 100644
index 000000000000..1c4cd096a51f
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-header.scss
@@ -0,0 +1,21 @@
+@import '@material/tab-indicator/mixins';
+@import '../../material/core/style/noop-animation';
+@import '../mdc-helpers/mdc-helpers';
+@import './tabs-common';
+
+@include mdc-tab-indicator-core-styles($query: $mat-base-styles-query);
+@include mat-mdc-paginated-tab-header;
+
+.mat-mdc-tab-label-container {
+ @include mat-mdc-paginated-tab-header-container;
+}
+
+.mat-mdc-tab-header-pagination-disabled {
+ box-shadow: none;
+ cursor: default;
+ opacity: 0.4;
+}
+
+.mat-mdc-tab-labels {
+ @include mat-mdc-paginated-tab-header-item-wrapper;
+}
diff --git a/src/material-experimental/mdc-tabs/tab-header.spec.ts b/src/material-experimental/mdc-tabs/tab-header.spec.ts
new file mode 100644
index 000000000000..3800074bd7ec
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-header.spec.ts
@@ -0,0 +1,656 @@
+import {Direction, Directionality} from '@angular/cdk/bidi';
+import {END, ENTER, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE} from '@angular/cdk/keycodes';
+import {PortalModule} from '@angular/cdk/portal';
+import {ScrollingModule, ViewportRuler} from '@angular/cdk/scrolling';
+import {
+ dispatchFakeEvent,
+ dispatchKeyboardEvent,
+ createKeyboardEvent,
+ dispatchEvent,
+} from '@angular/cdk/testing';
+import {CommonModule} from '@angular/common';
+import {Component, ViewChild} from '@angular/core';
+import {
+ async,
+ ComponentFixture,
+ discardPeriodicTasks,
+ fakeAsync,
+ TestBed,
+ tick,
+} from '@angular/core/testing';
+import {MatRippleModule} from '@angular/material/core';
+import {By} from '@angular/platform-browser';
+import {MatTabHeader} from './tab-header';
+import {MatTabLabelWrapper} from './tab-label-wrapper';
+import {Subject} from 'rxjs';
+import {ObserversModule, MutationObserverFactory} from '@angular/cdk/observers';
+
+
+describe('MatTabHeader', () => {
+ let dir: Direction = 'ltr';
+ let change = new Subject();
+ let fixture: ComponentFixture;
+ let appComponent: SimpleTabHeaderApp;
+
+ beforeEach(async(() => {
+ dir = 'ltr';
+ TestBed.configureTestingModule({
+ imports: [CommonModule, PortalModule, MatRippleModule, ScrollingModule, ObserversModule],
+ declarations: [
+ MatTabHeader,
+ MatTabLabelWrapper,
+ SimpleTabHeaderApp,
+ ],
+ providers: [
+ ViewportRuler,
+ {provide: Directionality, useFactory: () => ({value: dir, change: change.asObservable()})},
+ ]
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ describe('focusing', () => {
+ let tabListContainer: HTMLElement;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SimpleTabHeaderApp);
+ fixture.detectChanges();
+
+ appComponent = fixture.componentInstance;
+ tabListContainer = appComponent.tabHeader._tabListContainer.nativeElement;
+ });
+
+ it('should initialize to the selected index', () => {
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(appComponent.selectedIndex);
+ });
+
+ it('should send focus change event', () => {
+ appComponent.tabHeader.focusIndex = 2;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(2);
+ });
+
+ it('should not set focus a disabled tab', () => {
+ appComponent.tabHeader.focusIndex = 0;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+
+ // Set focus on the disabled tab, but focus should remain 0
+ appComponent.tabHeader.focusIndex = appComponent.disabledTabIndex;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+ });
+
+ it('should move focus right and skip disabled tabs', () => {
+ appComponent.tabHeader.focusIndex = 0;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+
+ // Move focus right, verify that the disabled tab is 1 and should be skipped
+ expect(appComponent.disabledTabIndex).toBe(1);
+ dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW);
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(2);
+
+ // Move focus right to index 3
+ dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW);
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(3);
+ });
+
+ it('should move focus left and skip disabled tabs', () => {
+ appComponent.tabHeader.focusIndex = 3;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(3);
+
+ // Move focus left to index 3
+ dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW);
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(2);
+
+ // Move focus left, verify that the disabled tab is 1 and should be skipped
+ expect(appComponent.disabledTabIndex).toBe(1);
+ dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW);
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+ });
+
+ it('should support key down events to move and select focus', () => {
+ appComponent.tabHeader.focusIndex = 0;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+
+ // Move focus right to 2
+ dispatchKeyboardEvent(tabListContainer, 'keydown', RIGHT_ARROW);
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(2);
+
+ // Select the focused index 2
+ expect(appComponent.selectedIndex).toBe(0);
+ const enterEvent = dispatchKeyboardEvent(tabListContainer, 'keydown', ENTER);
+ fixture.detectChanges();
+ expect(appComponent.selectedIndex).toBe(2);
+ expect(enterEvent.defaultPrevented).toBe(true);
+
+ // Move focus right to 0
+ dispatchKeyboardEvent(tabListContainer, 'keydown', LEFT_ARROW);
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+
+ // Select the focused 0 using space.
+ expect(appComponent.selectedIndex).toBe(2);
+ const spaceEvent = dispatchKeyboardEvent(tabListContainer, 'keydown', SPACE);
+ fixture.detectChanges();
+ expect(appComponent.selectedIndex).toBe(0);
+ expect(spaceEvent.defaultPrevented).toBe(true);
+ });
+
+ it('should move focus to the first tab when pressing HOME', () => {
+ appComponent.tabHeader.focusIndex = 3;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(3);
+
+ const event = dispatchKeyboardEvent(tabListContainer, 'keydown', HOME);
+ fixture.detectChanges();
+
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+ expect(event.defaultPrevented).toBe(true);
+ });
+
+ it('should skip disabled items when moving focus using HOME', () => {
+ appComponent.tabHeader.focusIndex = 3;
+ appComponent.tabs[0].disabled = true;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(3);
+
+ dispatchKeyboardEvent(tabListContainer, 'keydown', HOME);
+ fixture.detectChanges();
+
+ // Note that the second tab is disabled by default already.
+ expect(appComponent.tabHeader.focusIndex).toBe(2);
+ });
+
+ it('should move focus to the last tab when pressing END', () => {
+ appComponent.tabHeader.focusIndex = 0;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+
+ const event = dispatchKeyboardEvent(tabListContainer, 'keydown', END);
+ fixture.detectChanges();
+
+ expect(appComponent.tabHeader.focusIndex).toBe(3);
+ expect(event.defaultPrevented).toBe(true);
+ });
+
+ it('should skip disabled items when moving focus using END', () => {
+ appComponent.tabHeader.focusIndex = 0;
+ appComponent.tabs[3].disabled = true;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+
+ dispatchKeyboardEvent(tabListContainer, 'keydown', END);
+ fixture.detectChanges();
+
+ expect(appComponent.tabHeader.focusIndex).toBe(2);
+ });
+
+ it('should not do anything if a modifier key is pressed', () => {
+ const rightArrowEvent = createKeyboardEvent('keydown', RIGHT_ARROW);
+ const enterEvent = createKeyboardEvent('keydown', ENTER);
+
+ [rightArrowEvent, enterEvent].forEach(event => {
+ Object.defineProperty(event, 'shiftKey', {get: () => true});
+ });
+
+ appComponent.tabHeader.focusIndex = 0;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+
+ dispatchEvent(tabListContainer, rightArrowEvent);
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.focusIndex).toBe(0);
+ expect(rightArrowEvent.defaultPrevented).toBe(false);
+
+ expect(appComponent.selectedIndex).toBe(0);
+ dispatchEvent(tabListContainer, enterEvent);
+ fixture.detectChanges();
+ expect(appComponent.selectedIndex).toBe(0);
+ expect(enterEvent.defaultPrevented).toBe(false);
+ });
+
+ });
+
+ describe('pagination', () => {
+ describe('ltr', () => {
+ beforeEach(() => {
+ dir = 'ltr';
+ fixture = TestBed.createComponent(SimpleTabHeaderApp);
+ fixture.detectChanges();
+
+ appComponent = fixture.componentInstance;
+ });
+
+ it('should show width when tab list width exceeds container', () => {
+ 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(true);
+ });
+
+ it('should scroll to show the focused tab label', () => {
+ appComponent.addTabsForScrolling();
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.scrollDistance).toBe(0);
+
+ // Focus on the last tab, expect this to be the maximum scroll distance.
+ appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.scrollDistance)
+ .toBe(appComponent.tabHeader._getMaxScrollDistance());
+
+ // Focus on the first tab, expect this to be the maximum scroll distance.
+ appComponent.tabHeader.focusIndex = 0;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.scrollDistance).toBe(0);
+ });
+
+ it('should show ripples for pagination buttons', () => {
+ appComponent.addTabsForScrolling();
+ fixture.detectChanges();
+
+ expect(appComponent.tabHeader._showPaginationControls).toBe(true);
+
+ const buttonAfter =
+ fixture.debugElement.query(By.css('.mat-mdc-tab-header-pagination-after'));
+
+ expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
+ .toBe(0, 'Expected no ripple to show up initially.');
+
+ dispatchFakeEvent(buttonAfter.nativeElement, 'mousedown');
+ dispatchFakeEvent(buttonAfter.nativeElement, 'mouseup');
+
+ expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
+ .toBe(1, 'Expected one ripple to show up after mousedown');
+ });
+
+ it('should allow disabling ripples for pagination buttons', () => {
+ appComponent.addTabsForScrolling();
+ appComponent.disableRipple = true;
+ fixture.detectChanges();
+
+ expect(appComponent.tabHeader._showPaginationControls).toBe(true);
+
+ const buttonAfter =
+ fixture.debugElement.query(By.css('.mat-mdc-tab-header-pagination-after'));
+
+ expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
+ .toBe(0, 'Expected no ripple to show up initially.');
+
+ dispatchFakeEvent(buttonAfter.nativeElement, 'mousedown');
+ dispatchFakeEvent(buttonAfter.nativeElement, 'mouseup');
+
+ expect(fixture.nativeElement.querySelectorAll('.mat-ripple-element').length)
+ .toBe(0, 'Expected no ripple to show up after mousedown');
+ });
+
+ });
+
+ describe('rtl', () => {
+ beforeEach(() => {
+ dir = 'rtl';
+ fixture = TestBed.createComponent(SimpleTabHeaderApp);
+ appComponent = fixture.componentInstance;
+ appComponent.dir = 'rtl';
+
+ fixture.detectChanges();
+ });
+
+ it('should scroll to show the focused tab label', () => {
+ appComponent.addTabsForScrolling();
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.scrollDistance).toBe(0);
+
+ // Focus on the last tab, expect this to be the maximum scroll distance.
+ appComponent.tabHeader.focusIndex = appComponent.tabs.length - 1;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.scrollDistance)
+ .toBe(appComponent.tabHeader._getMaxScrollDistance());
+
+ // Focus on the first tab, expect this to be the maximum scroll distance.
+ appComponent.tabHeader.focusIndex = 0;
+ fixture.detectChanges();
+ expect(appComponent.tabHeader.scrollDistance).toBe(0);
+ });
+ });
+
+ describe('scrolling when holding paginator', () => {
+ let nextButton: HTMLElement;
+ let prevButton: HTMLElement;
+ let header: MatTabHeader;
+ let headerElement: HTMLElement;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SimpleTabHeaderApp);
+ fixture.componentInstance.disableRipple = true;
+ fixture.detectChanges();
+
+ fixture.componentInstance.addTabsForScrolling(50);
+ fixture.detectChanges();
+
+ nextButton = fixture.nativeElement.querySelector('.mat-mdc-tab-header-pagination-after');
+ prevButton = fixture.nativeElement.querySelector('.mat-mdc-tab-header-pagination-before');
+ header = fixture.componentInstance.tabHeader;
+ headerElement = fixture.nativeElement.querySelector('.mat-mdc-tab-header');
+ });
+
+ it('should scroll towards the end while holding down the next button using a mouse',
+ fakeAsync(() => {
+ assertNextButtonScrolling('mousedown', 'click');
+ }));
+
+ it('should scroll towards the start while holding down the prev button using a mouse',
+ fakeAsync(() => {
+ assertPrevButtonScrolling('mousedown', 'click');
+ }));
+
+ it('should scroll towards the end while holding down the next button using touch',
+ fakeAsync(() => {
+ assertNextButtonScrolling('touchstart', 'touchend');
+ }));
+
+ it('should scroll towards the start while holding down the prev button using touch',
+ fakeAsync(() => {
+ assertPrevButtonScrolling('touchstart', 'touchend');
+ }));
+
+ it('should not scroll if the sequence is interrupted quickly', fakeAsync(() => {
+ expect(header.scrollDistance).toBe(0, 'Expected to start off not scrolled.');
+
+ dispatchFakeEvent(nextButton, 'mousedown');
+ fixture.detectChanges();
+
+ tick(100);
+
+ dispatchFakeEvent(headerElement, 'mouseleave');
+ fixture.detectChanges();
+
+ tick(3000);
+
+ expect(header.scrollDistance).toBe(0, 'Expected not to have scrolled after a while.');
+ }));
+
+ it('should clear the timeouts on destroy', fakeAsync(() => {
+ dispatchFakeEvent(nextButton, 'mousedown');
+ fixture.detectChanges();
+ fixture.destroy();
+
+ // No need to assert. If fakeAsync doesn't throw, it means that the timers were cleared.
+ }));
+
+ it('should clear the timeouts on click', fakeAsync(() => {
+ dispatchFakeEvent(nextButton, 'mousedown');
+ fixture.detectChanges();
+
+ dispatchFakeEvent(nextButton, 'click');
+ fixture.detectChanges();
+
+ // No need to assert. If fakeAsync doesn't throw, it means that the timers were cleared.
+ }));
+
+ it('should clear the timeouts on touchend', fakeAsync(() => {
+ dispatchFakeEvent(nextButton, 'touchstart');
+ fixture.detectChanges();
+
+ dispatchFakeEvent(nextButton, 'touchend');
+ fixture.detectChanges();
+
+ // No need to assert. If fakeAsync doesn't throw, it means that the timers were cleared.
+ }));
+
+ it('should clear the timeouts when reaching the end', fakeAsync(() => {
+ dispatchFakeEvent(nextButton, 'mousedown');
+ fixture.detectChanges();
+
+ // Simulate a very long timeout.
+ tick(60000);
+
+ // No need to assert. If fakeAsync doesn't throw, it means that the timers were cleared.
+ }));
+
+ it('should clear the timeouts when reaching the start', fakeAsync(() => {
+ header.scrollDistance = Infinity;
+ fixture.detectChanges();
+
+ dispatchFakeEvent(prevButton, 'mousedown');
+ fixture.detectChanges();
+
+ // Simulate a very long timeout.
+ tick(60000);
+
+ // No need to assert. If fakeAsync doesn't throw, it means that the timers were cleared.
+ }));
+
+ it('should stop scrolling if the pointer leaves the header', fakeAsync(() => {
+ expect(header.scrollDistance).toBe(0, 'Expected to start off not scrolled.');
+
+ dispatchFakeEvent(nextButton, 'mousedown');
+ fixture.detectChanges();
+ tick(300);
+
+ expect(header.scrollDistance).toBe(0, 'Expected not to scroll after short amount of time.');
+
+ tick(1000);
+
+ expect(header.scrollDistance).toBeGreaterThan(0, 'Expected to scroll after some time.');
+
+ let previousDistance = header.scrollDistance;
+
+ dispatchFakeEvent(headerElement, 'mouseleave');
+ fixture.detectChanges();
+ tick(100);
+
+ expect(header.scrollDistance).toBe(previousDistance);
+ }));
+
+ /**
+ * Asserts that auto scrolling using the next button works.
+ * @param startEventName Name of the event that is supposed to start the scrolling.
+ * @param endEventName Name of the event that is supposed to end the scrolling.
+ */
+ function assertNextButtonScrolling(startEventName: string, endEventName: string) {
+ expect(header.scrollDistance).toBe(0, 'Expected to start off not scrolled.');
+
+ dispatchFakeEvent(nextButton, startEventName);
+ fixture.detectChanges();
+ tick(300);
+
+ expect(header.scrollDistance).toBe(0, 'Expected not to scroll after short amount of time.');
+
+ tick(1000);
+
+ expect(header.scrollDistance).toBeGreaterThan(0, 'Expected to scroll after some time.');
+
+ let previousDistance = header.scrollDistance;
+
+ tick(100);
+
+ expect(header.scrollDistance)
+ .toBeGreaterThan(previousDistance, 'Expected to scroll again after some more time.');
+
+ dispatchFakeEvent(nextButton, endEventName);
+ }
+
+ /**
+ * Asserts that auto scrolling using the previous button works.
+ * @param startEventName Name of the event that is supposed to start the scrolling.
+ * @param endEventName Name of the event that is supposed to end the scrolling.
+ */
+ function assertPrevButtonScrolling(startEventName: string, endEventName: string) {
+ header.scrollDistance = Infinity;
+ fixture.detectChanges();
+
+ let currentScroll = header.scrollDistance;
+
+ expect(currentScroll).toBeGreaterThan(0, 'Expected to start off scrolled.');
+
+ dispatchFakeEvent(prevButton, startEventName);
+ fixture.detectChanges();
+ tick(300);
+
+ expect(header.scrollDistance)
+ .toBe(currentScroll, 'Expected not to scroll after short amount of time.');
+
+ tick(1000);
+
+ expect(header.scrollDistance)
+ .toBeLessThan(currentScroll, 'Expected to scroll after some time.');
+
+ currentScroll = header.scrollDistance;
+
+ tick(100);
+
+ expect(header.scrollDistance)
+ .toBeLessThan(currentScroll, 'Expected to scroll again after some more time.');
+
+ dispatchFakeEvent(nextButton, endEventName);
+ }
+
+ });
+
+ it('should re-align the ink bar when the direction changes', fakeAsync(() => {
+ fixture = TestBed.createComponent(SimpleTabHeaderApp);
+ fixture.detectChanges();
+
+ const inkBar = fixture.componentInstance.tabHeader._inkBar;
+ spyOn(inkBar, 'alignToElement');
+
+ fixture.detectChanges();
+
+ change.next();
+ fixture.detectChanges();
+ tick(20); // Angular turns rAF calls into 16.6ms timeouts in tests.
+
+ expect(inkBar.alignToElement).toHaveBeenCalled();
+ }));
+
+ it('should re-align the ink bar when the window is resized', fakeAsync(() => {
+ fixture = TestBed.createComponent(SimpleTabHeaderApp);
+ fixture.detectChanges();
+
+ const inkBar = fixture.componentInstance.tabHeader._inkBar;
+
+ spyOn(inkBar, 'alignToElement');
+
+ dispatchFakeEvent(window, 'resize');
+ tick(150);
+ fixture.detectChanges();
+
+ expect(inkBar.alignToElement).toHaveBeenCalled();
+ discardPeriodicTasks();
+ }));
+
+ it('should update arrows when the window is resized', fakeAsync(() => {
+ fixture = TestBed.createComponent(SimpleTabHeaderApp);
+
+ const header = fixture.componentInstance.tabHeader;
+
+ spyOn(header, '_checkPaginationEnabled');
+
+ dispatchFakeEvent(window, 'resize');
+ tick(10);
+ fixture.detectChanges();
+
+ expect(header._checkPaginationEnabled).toHaveBeenCalled();
+ discardPeriodicTasks();
+ }));
+
+ it('should update the pagination state if the content of the labels changes', () => {
+ const mutationCallbacks: Function[] = [];
+ TestBed.overrideProvider(MutationObserverFactory, {
+ useValue: {
+ // Stub out the MutationObserver since the native one is async.
+ create: function(callback: Function) {
+ mutationCallbacks.push(callback);
+ return {observe: () => {}, disconnect: () => {}};
+ }
+ }
+ });
+
+ fixture = TestBed.createComponent(SimpleTabHeaderApp);
+ fixture.detectChanges();
+
+ const tabHeaderElement: HTMLElement =
+ fixture.nativeElement.querySelector('.mat-mdc-tab-header');
+ const labels =
+ Array.from(fixture.nativeElement.querySelectorAll('.label-content'));
+ const extraText = new Array(100).fill('w').join();
+ const enabledClass = 'mat-mdc-tab-header-pagination-controls-enabled';
+
+ expect(tabHeaderElement.classList).not.toContain(enabledClass);
+
+ labels.forEach(label => {
+ label.style.width = '';
+ label.textContent += extraText;
+ });
+
+ mutationCallbacks.forEach(callback => callback());
+ fixture.detectChanges();
+
+ expect(tabHeaderElement.classList).toContain(enabledClass);
+ });
+
+ });
+});
+
+interface Tab {
+ label: string;
+ disabled?: boolean;
+}
+
+@Component({
+ template: `
+
+
+
+ {{tab.label}}
+
+
+
+ `,
+ styles: [`
+ :host {
+ width: 130px;
+ }
+ `]
+})
+class SimpleTabHeaderApp {
+ disableRipple: boolean = false;
+ selectedIndex: number = 0;
+ focusedIndex: number;
+ disabledTabIndex = 1;
+ tabs: Tab[] = [{label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}, {label: 'tab one'}];
+ dir: Direction = 'ltr';
+
+ @ViewChild(MatTabHeader, {static: true}) tabHeader: MatTabHeader;
+
+ constructor() {
+ this.tabs[this.disabledTabIndex].disabled = true;
+ }
+
+ addTabsForScrolling(amount = 4) {
+ for (let i = 0; i < amount; i++) {
+ this.tabs.push({label: 'new'});
+ }
+ }
+}
diff --git a/src/material-experimental/mdc-tabs/tab-header.ts b/src/material-experimental/mdc-tabs/tab-header.ts
new file mode 100644
index 000000000000..82c5481ad8c3
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-header.ts
@@ -0,0 +1,76 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ ChangeDetectionStrategy,
+ Component,
+ ViewEncapsulation,
+ ContentChildren,
+ ViewChild,
+ ElementRef,
+ QueryList,
+ AfterContentInit,
+ Optional,
+ ChangeDetectorRef,
+ NgZone,
+ Inject,
+} from '@angular/core';
+import {_MatTabHeaderBase} from '@angular/material/tabs';
+import {ViewportRuler} from '@angular/cdk/scrolling';
+import {Platform} from '@angular/cdk/platform';
+import {Directionality} from '@angular/cdk/bidi';
+import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
+import {MatTabLabelWrapper} from './tab-label-wrapper';
+import {MatInkBar} from './ink-bar';
+
+/**
+ * The header of the tab group which displays a list of all the tabs in the tab group. Includes
+ * an ink bar that follows the currently selected tab. When the tabs list's width exceeds the
+ * width of the header container, then arrows will be displayed to allow the user to scroll
+ * left and right across the header.
+ * @docs-private
+ */
+@Component({
+ moduleId: module.id,
+ selector: 'mat-tab-header',
+ templateUrl: 'tab-header.html',
+ styleUrls: ['tab-header.css'],
+ inputs: ['selectedIndex'],
+ outputs: ['selectFocusedIndex', 'indexFocused'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ 'class': 'mat-mdc-tab-header',
+ '[class.mat-mdc-tab-header-pagination-controls-enabled]': '_showPaginationControls',
+ '[class.mat-mdc-tab-header-rtl]': "_getLayoutDirection() == 'rtl'",
+ },
+})
+export class MatTabHeader extends _MatTabHeaderBase implements AfterContentInit {
+ @ContentChildren(MatTabLabelWrapper) _items: QueryList;
+ @ViewChild('tabListContainer', {static: true}) _tabListContainer: ElementRef;
+ @ViewChild('tabList', {static: true}) _tabList: ElementRef;
+ @ViewChild('nextPaginator', {static: false}) _nextPaginator: ElementRef;
+ @ViewChild('previousPaginator', {static: false}) _previousPaginator: ElementRef;
+ _inkBar: MatInkBar;
+
+ constructor(elementRef: ElementRef,
+ changeDetectorRef: ChangeDetectorRef,
+ viewportRuler: ViewportRuler,
+ @Optional() dir: Directionality,
+ ngZone: NgZone,
+ platform: Platform,
+ // @breaking-change 9.0.0 `_animationMode` parameter to be made required.
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
+ super(elementRef, changeDetectorRef, viewportRuler, dir, ngZone, platform, animationMode);
+ }
+
+ ngAfterContentInit() {
+ this._inkBar = new MatInkBar(this._items);
+ super.ngAfterContentInit();
+ }
+}
diff --git a/src/material-experimental/mdc-tabs/tab-label-wrapper.ts b/src/material-experimental/mdc-tabs/tab-label-wrapper.ts
new file mode 100644
index 000000000000..79cc82ae5e11
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-label-wrapper.ts
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive, ElementRef, Inject, OnDestroy} from '@angular/core';
+import {DOCUMENT} from '@angular/common';
+import {MatTabLabelWrapper as BaseMatTabLabelWrapper} from '@angular/material/tabs';
+import {MatInkBarFoundation, MatInkBarItem} from './ink-bar';
+
+/**
+ * Used in the `mat-tab-group` view to display tab labels.
+ * @docs-private
+ */
+@Directive({
+ selector: '[matTabLabelWrapper]',
+ inputs: ['disabled'],
+ host: {
+ '[class.mat-mdc-tab-disabled]': 'disabled',
+ '[attr.aria-disabled]': '!!disabled',
+ }
+})
+export class MatTabLabelWrapper extends BaseMatTabLabelWrapper implements MatInkBarItem, OnDestroy {
+ _foundation: MatInkBarFoundation;
+
+ constructor(public elementRef: ElementRef, @Inject(DOCUMENT) _document: any) {
+ super(elementRef);
+ this._foundation = new MatInkBarFoundation(elementRef, _document);
+ this._foundation.init();
+ }
+
+ ngOnDestroy() {
+ this._foundation.destroy();
+ }
+
+ /** Sets focus on the wrapper element */
+ focus(): void {
+ this.elementRef.nativeElement.focus();
+ }
+}
diff --git a/src/material-experimental/mdc-tabs/tab-label.ts b/src/material-experimental/mdc-tabs/tab-label.ts
new file mode 100644
index 000000000000..d335d9052adb
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-label.ts
@@ -0,0 +1,16 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {Directive} from '@angular/core';
+import {MatTabLabel as BaseMatTabLabel} from '@angular/material/tabs';
+
+/** Used to flag tab labels for use with the portal directive */
+@Directive({
+ selector: '[mat-tab-label], [matTabLabel]',
+})
+export class MatTabLabel extends BaseMatTabLabel {}
diff --git a/src/material-experimental/mdc-tabs/tab-nav-bar/tab-link.html b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-link.html
new file mode 100644
index 000000000000..6c03e75574e9
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-link.html
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
diff --git a/src/material-experimental/mdc-tabs/tab-nav-bar/tab-link.scss b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-link.scss
new file mode 100644
index 000000000000..5c203f41c47c
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-link.scss
@@ -0,0 +1,36 @@
+@import '@material/tab/mixins';
+@import '../../../material/core/style/variables';
+@import '../../mdc-helpers/mdc-helpers';
+@import '../tabs-common';
+
+@include mdc-tab-without-ripple($query: $mat-base-styles-query);
+@include mdc-tab-indicator-core-styles($query: $mat-base-styles-query);
+@include mat-mdc-tab-ripple;
+
+// Wraps each link in the header
+.mat-mdc-tab-link {
+ @include mat-mdc-tab;
+
+ &.mat-mdc-tab-disabled {
+ // We use `pointer-events` to make the element unclickable when it's disabled, rather than
+ // preventing the default action through JS, because we can't prevent the action reliably
+ // due to other directives potentially registering their events earlier. This shouldn't cause
+ // the user to click through, because we always have a `.mat-tab-links` behind the link.
+ pointer-events: none;
+
+ // MDC doesn't support disabled tabs so we need to improvise.
+ opacity: 0.4;
+ }
+
+ // Note that we only want to target direct descendant tabs. Also note that
+ // `mat-stretch-tabs` is part of the public API so it should not be changed to `mat-mdc-`.
+ .mat-mdc-tab-header[mat-stretch-tabs] & {
+ flex-grow: 1;
+ }
+}
+
+@media ($mat-xsmall) {
+ .mat-mdc-tab-link {
+ min-width: 72px;
+ }
+}
diff --git a/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.html b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.html
new file mode 100644
index 000000000000..8b77be104601
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.html
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.scss b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.scss
new file mode 100644
index 000000000000..3fa3c7997a0b
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.scss
@@ -0,0 +1,14 @@
+@import '@material/tab/mixins';
+@import '../tabs-common';
+@import '../../../material/core/style/variables';
+@import '../../mdc-helpers/mdc-helpers';
+
+@include mat-mdc-paginated-tab-header;
+
+.mat-mdc-tab-links {
+ @include mat-mdc-paginated-tab-header-item-wrapper;
+}
+
+.mat-mdc-tab-link-container {
+ @include mat-mdc-paginated-tab-header-container;
+}
diff --git a/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.spec.ts b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.spec.ts
new file mode 100644
index 000000000000..9d95ac756271
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.spec.ts
@@ -0,0 +1,395 @@
+import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
+import {Component, ViewChild, ViewChildren, QueryList} from '@angular/core';
+import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core';
+import {By} from '@angular/platform-browser';
+import {dispatchFakeEvent, dispatchMouseEvent} from '@angular/cdk/testing';
+import {Direction, Directionality} from '@angular/cdk/bidi';
+import {Subject} from 'rxjs';
+import {MatTabsModule} from '../module';
+import {MatTabLink, MatTabNav} from './tab-nav-bar';
+
+
+describe('MatTabNavBar', () => {
+ let dir: Direction = 'ltr';
+ let dirChange = new Subject();
+ let globalRippleOptions: RippleGlobalOptions;
+
+ beforeEach(async(() => {
+ globalRippleOptions = {};
+
+ TestBed.configureTestingModule({
+ imports: [MatTabsModule],
+ declarations: [
+ SimpleTabNavBarTestApp,
+ TabLinkWithNgIf,
+ TabLinkWithTabIndexBinding,
+ TabLinkWithNativeTabindexAttr,
+ TabBarWithInactiveTabsOnInit,
+ ],
+ providers: [
+ {provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions},
+ {provide: Directionality, useFactory: () =>
+ ({value: dir, change: dirChange.asObservable()})},
+ ]
+ });
+
+ TestBed.compileComponents();
+ }));
+
+ describe('basic behavior', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
+ fixture.detectChanges();
+ });
+
+ it('should change active index on click', () => {
+ // select the second link
+ let tabLink = fixture.debugElement.queryAll(By.css('a'))[1];
+ tabLink.nativeElement.click();
+ expect(fixture.componentInstance.activeIndex).toBe(1);
+
+ // select the third link
+ tabLink = fixture.debugElement.queryAll(By.css('a'))[2];
+ tabLink.nativeElement.click();
+ expect(fixture.componentInstance.activeIndex).toBe(2);
+ });
+
+ it('should add the active class if active', () => {
+ let tabLink1 = fixture.debugElement.queryAll(By.css('a'))[0];
+ let tabLink2 = fixture.debugElement.queryAll(By.css('a'))[1];
+ const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
+ .map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
+
+ tabLink1.nativeElement.click();
+ fixture.detectChanges();
+ expect(tabLinkElements[0].classList.contains('mdc-tab--active')).toBeTruthy();
+ expect(tabLinkElements[1].classList.contains('mdc-tab--active')).toBeFalsy();
+
+ tabLink2.nativeElement.click();
+ fixture.detectChanges();
+ expect(tabLinkElements[0].classList.contains('mdc-tab--active')).toBeFalsy();
+ expect(tabLinkElements[1].classList.contains('mdc-tab--active')).toBeTruthy();
+ });
+
+ it('should toggle aria-current based on active state', () => {
+ let tabLink1 = fixture.debugElement.queryAll(By.css('a'))[0];
+ let tabLink2 = fixture.debugElement.queryAll(By.css('a'))[1];
+ const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
+ .map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
+
+ tabLink1.nativeElement.click();
+ fixture.detectChanges();
+ expect(tabLinkElements[0].getAttribute('aria-current')).toEqual('page');
+ expect(tabLinkElements[1].hasAttribute('aria-current')).toEqual(false);
+
+ tabLink2.nativeElement.click();
+ fixture.detectChanges();
+ expect(tabLinkElements[0].hasAttribute('aria-current')).toEqual(false);
+ expect(tabLinkElements[1].getAttribute('aria-current')).toEqual('page');
+ });
+
+ it('should add the disabled class if disabled', () => {
+ const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
+ .map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
+
+ expect(tabLinkElements.every(tabLinkEl => {
+ return !tabLinkEl.classList.contains('mat-mdc-tab-disabled');
+ })).toBe(true, 'Expected every tab link to not have the disabled class initially');
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ expect(tabLinkElements.every(tabLinkEl => {
+ return tabLinkEl.classList.contains('mat-mdc-tab-disabled');
+ })).toBe(true, 'Expected every tab link to have the disabled class if set through binding');
+ });
+
+ it('should update aria-disabled if disabled', () => {
+ const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
+ .map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
+
+ expect(tabLinkElements.every(tabLink => tabLink.getAttribute('aria-disabled') === 'false'))
+ .toBe(true, 'Expected aria-disabled to be set to "false" by default.');
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ expect(tabLinkElements.every(tabLink => tabLink.getAttribute('aria-disabled') === 'true'))
+ .toBe(true, 'Expected aria-disabled to be set to "true" if link is disabled.');
+ });
+
+ it('should update the tabindex if links are disabled', () => {
+ const tabLinkElements = fixture.debugElement.queryAll(By.css('a'))
+ .map(tabLinkDebugEl => tabLinkDebugEl.nativeElement);
+
+ expect(tabLinkElements.every(tabLink => tabLink.tabIndex === 0))
+ .toBe(true, 'Expected element to be keyboard focusable by default');
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ expect(tabLinkElements.every(tabLink => tabLink.tabIndex === -1))
+ .toBe(true, 'Expected element to no longer be keyboard focusable if disabled.');
+ });
+
+ it('should mark disabled links', () => {
+ const tabLinkElement = fixture.debugElement.query(By.css('a')).nativeElement;
+
+ expect(tabLinkElement.classList).not.toContain('mat-mdc-tab-disabled');
+
+ fixture.componentInstance.disabled = true;
+ fixture.detectChanges();
+
+ expect(tabLinkElement.classList).toContain('mat-mdc-tab-disabled');
+ });
+
+ it('should re-align the ink bar when the direction changes', () => {
+ const inkBar = fixture.componentInstance.tabNavBar._inkBar;
+
+ spyOn(inkBar, 'alignToElement');
+
+ dirChange.next();
+ fixture.detectChanges();
+
+ expect(inkBar.alignToElement).toHaveBeenCalled();
+ });
+
+ it('should re-align the ink bar when the tabs list change', () => {
+ const inkBar = fixture.componentInstance.tabNavBar._inkBar;
+
+ spyOn(inkBar, 'alignToElement');
+
+ fixture.componentInstance.tabs = [1, 2, 3, 4];
+ fixture.detectChanges();
+
+ expect(inkBar.alignToElement).toHaveBeenCalled();
+ });
+
+ it('should re-align the ink bar when the tab labels change the width', done => {
+ const inkBar = fixture.componentInstance.tabNavBar._inkBar;
+
+ const spy = spyOn(inkBar, 'alignToElement').and.callFake(() => {
+ expect(spy.calls.any()).toBe(true);
+ done();
+ });
+
+ fixture.componentInstance.label = 'label change';
+ fixture.detectChanges();
+
+ expect(spy.calls.any()).toBe(false);
+ });
+
+ it('should re-align the ink bar when the window is resized', fakeAsync(() => {
+ const inkBar = fixture.componentInstance.tabNavBar._inkBar;
+
+ spyOn(inkBar, 'alignToElement');
+
+ dispatchFakeEvent(window, 'resize');
+ tick(150);
+ fixture.detectChanges();
+
+ expect(inkBar.alignToElement).toHaveBeenCalled();
+ }));
+
+ it('should hide the ink bar when all the links are inactive', () => {
+ const inkBar = fixture.componentInstance.tabNavBar._inkBar;
+
+ spyOn(inkBar, 'hide');
+
+ fixture.componentInstance.tabLinks.forEach(link => link.active = false);
+ fixture.detectChanges();
+
+ expect(inkBar.hide).toHaveBeenCalled();
+ });
+
+ });
+
+ it('should hide the ink bar if no tabs are active on init', fakeAsync(() => {
+ const fixture = TestBed.createComponent(TabBarWithInactiveTabsOnInit);
+ fixture.detectChanges();
+ tick(20); // Angular turns rAF calls into 16.6ms timeouts in tests.
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.querySelectorAll('.mdc-tab-indicator--active').length).toBe(0);
+ }));
+
+ it('should clean up the ripple event handlers on destroy', () => {
+ let fixture: ComponentFixture = TestBed.createComponent(TabLinkWithNgIf);
+ fixture.detectChanges();
+
+ let link = fixture.debugElement.nativeElement.querySelector('.mat-mdc-tab-link');
+
+ fixture.componentInstance.isDestroyed = true;
+ fixture.detectChanges();
+
+ dispatchMouseEvent(link, 'mousedown');
+
+ expect(link.querySelector('.mat-ripple-element'))
+ .toBeFalsy('Expected no ripple to be created when ripple target is destroyed.');
+ });
+
+ it('should support the native tabindex attribute', () => {
+ const fixture = TestBed.createComponent(TabLinkWithNativeTabindexAttr);
+ fixture.detectChanges();
+
+ const tabLink = fixture.debugElement.query(By.directive(MatTabLink))
+ .injector.get(MatTabLink);
+
+ expect(tabLink.tabIndex)
+ .toBe(5, 'Expected the tabIndex to be set from the native tabindex attribute.');
+ });
+
+ it('should support binding to the tabIndex', () => {
+ const fixture = TestBed.createComponent(TabLinkWithTabIndexBinding);
+ fixture.detectChanges();
+
+ const tabLink = fixture.debugElement.query(By.directive(MatTabLink))
+ .injector.get(MatTabLink);
+
+ expect(tabLink.tabIndex).toBe(0, 'Expected the tabIndex to be set to 0 by default.');
+
+ fixture.componentInstance.tabIndex = 3;
+ fixture.detectChanges();
+
+ expect(tabLink.tabIndex).toBe(3, 'Expected the tabIndex to be have been set to 3.');
+ });
+
+ describe('ripples', () => {
+ let fixture: ComponentFixture;
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
+ fixture.detectChanges();
+ });
+
+ it('should be disabled on all tab links when they are disabled on the nav bar', () => {
+ expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => !tabLink.rippleDisabled))
+ .toBe(true, 'Expected every tab link to have ripples enabled');
+
+ fixture.componentInstance.disableRippleOnBar = true;
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => tabLink.rippleDisabled))
+ .toBe(true, 'Expected every tab link to have ripples disabled');
+ });
+
+ it('should have the `disableRipple` from the tab take precedence over the nav bar', () => {
+ const firstTab = fixture.componentInstance.tabLinks.first;
+
+ expect(firstTab.rippleDisabled).toBe(false, 'Expected ripples to be enabled on first tab');
+
+ firstTab.disableRipple = true;
+ fixture.componentInstance.disableRippleOnBar = false;
+ fixture.detectChanges();
+
+ expect(firstTab.rippleDisabled).toBe(true, 'Expected ripples to be disabled on first tab');
+ });
+
+ it('should show up for tab link elements on mousedown', () => {
+ const tabLink = fixture.debugElement.nativeElement.querySelector('.mat-mdc-tab-link');
+
+ dispatchMouseEvent(tabLink, 'mousedown');
+ dispatchMouseEvent(tabLink, 'mouseup');
+
+ expect(tabLink.querySelectorAll('.mat-ripple-element').length)
+ .toBe(1, 'Expected one ripple to show up if user clicks on tab link.');
+ });
+
+ it('should be able to disable ripples on an individual tab link', () => {
+ const tabLinkDebug = fixture.debugElement.query(By.css('a'));
+ const tabLinkElement = tabLinkDebug.nativeElement;
+
+ fixture.componentInstance.disableRippleOnLink = true;
+ fixture.detectChanges();
+
+ dispatchMouseEvent(tabLinkElement, 'mousedown');
+ dispatchMouseEvent(tabLinkElement, 'mouseup');
+
+ expect(tabLinkElement.querySelectorAll('.mat-ripple-element').length)
+ .toBe(0, 'Expected no ripple to show up if ripples are disabled.');
+ });
+
+ it('should be able to disable ripples through global options at runtime', () => {
+ expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => !tabLink.rippleDisabled))
+ .toBe(true, 'Expected every tab link to have ripples enabled');
+
+ globalRippleOptions.disabled = true;
+
+ expect(fixture.componentInstance.tabLinks.toArray().every(tabLink => tabLink.rippleDisabled))
+ .toBe(true, 'Expected every tab link to have ripples disabled');
+ });
+ });
+});
+
+@Component({
+ selector: 'test-app',
+ template: `
+
+ `
+})
+class SimpleTabNavBarTestApp {
+ @ViewChild(MatTabNav, {static: false}) tabNavBar: MatTabNav;
+ @ViewChildren(MatTabLink) tabLinks: QueryList;
+
+ label = '';
+ disabled = false;
+ disableRippleOnBar = false;
+ disableRippleOnLink = false;
+ tabs = [0, 1, 2];
+
+ activeIndex = 0;
+}
+
+@Component({
+ template: `
+
+ `
+})
+class TabLinkWithNgIf {
+ isDestroyed = false;
+}
+
+@Component({
+ template: `
+
+ `
+})
+class TabLinkWithTabIndexBinding {
+ tabIndex = 0;
+}
+
+@Component({
+ template: `
+
+ `
+})
+class TabLinkWithNativeTabindexAttr {}
+
+
+@Component({
+ template: `
+
+ `
+})
+class TabBarWithInactiveTabsOnInit {
+ tabs = [0, 1, 2];
+}
diff --git a/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts
new file mode 100644
index 000000000000..90cf2225b471
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts
@@ -0,0 +1,125 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import {
+ ChangeDetectionStrategy,
+ Component,
+ ContentChildren,
+ ElementRef,
+ forwardRef,
+ QueryList,
+ ViewChild,
+ ViewEncapsulation,
+ Optional,
+ Inject,
+ Attribute,
+ OnDestroy,
+ AfterContentInit,
+ NgZone,
+ ChangeDetectorRef,
+} from '@angular/core';
+import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
+import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core';
+import {FocusMonitor} from '@angular/cdk/a11y';
+import {_MatTabNavBase, _MatTabLinkBase} from '@angular/material/tabs';
+import {DOCUMENT} from '@angular/common';
+import {Directionality} from '@angular/cdk/bidi';
+import {ViewportRuler} from '@angular/cdk/scrolling';
+import {Platform} from '@angular/cdk/platform';
+import {MatInkBar, MatInkBarItem, MatInkBarFoundation} from '../ink-bar';
+
+
+/**
+ * Navigation component matching the styles of the tab group header.
+ * Provides anchored navigation with animated ink bar.
+ */
+@Component({
+ moduleId: module.id,
+ selector: '[mat-tab-nav-bar]',
+ exportAs: 'matTabNavBar, matTabNav',
+ inputs: ['color'],
+ templateUrl: 'tab-nav-bar.html',
+ styleUrls: ['tab-nav-bar.css'],
+ host: {
+ 'class': 'mat-mdc-tab-nav-bar mat-mdc-tab-header',
+ '[class.mat-mdc-tab-header-pagination-controls-enabled]': '_showPaginationControls',
+ '[class.mat-mdc-tab-header-rtl]': "_getLayoutDirection() == 'rtl'",
+ '[class.mat-primary]': 'color !== "warn" && color !== "accent"',
+ '[class.mat-accent]': 'color === "accent"',
+ '[class.mat-warn]': 'color === "warn"',
+ },
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class MatTabNav extends _MatTabNavBase implements AfterContentInit {
+ @ContentChildren(forwardRef(() => MatTabLink), {descendants: true}) _items: QueryList;
+ @ViewChild('tabListContainer', {static: true}) _tabListContainer: ElementRef;
+ @ViewChild('tabList', {static: true}) _tabList: ElementRef;
+ @ViewChild('nextPaginator', {static: false}) _nextPaginator: ElementRef;
+ @ViewChild('previousPaginator', {static: false}) _previousPaginator: ElementRef;
+ _inkBar: MatInkBar;
+
+ constructor(elementRef: ElementRef,
+ @Optional() dir: Directionality,
+ ngZone: NgZone,
+ changeDetectorRef: ChangeDetectorRef,
+ viewportRuler: ViewportRuler,
+ /**
+ * @deprecated @breaking-change 9.0.0 `platform` parameter to become required.
+ */
+ @Optional() platform?: Platform,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
+ super(elementRef, dir, ngZone, changeDetectorRef, viewportRuler, platform, animationMode);
+ }
+
+ ngAfterContentInit() {
+ this._inkBar = new MatInkBar(this._items);
+ super.ngAfterContentInit();
+ }
+}
+
+/**
+ * Link inside of a `mat-tab-nav-bar`.
+ */
+@Component({
+ moduleId: module.id,
+ selector: '[mat-tab-link], [matTabLink]',
+ exportAs: 'matTabLink',
+ inputs: ['disabled', 'disableRipple', 'tabIndex'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ templateUrl: 'tab-link.html',
+ styleUrls: ['tab-link.css'],
+ host: {
+ 'class': 'mdc-tab mat-mdc-tab-link',
+ '[attr.aria-current]': 'active ? "page" : null',
+ '[attr.aria-disabled]': 'disabled',
+ '[attr.tabIndex]': 'tabIndex',
+ '[class.mat-mdc-tab-disabled]': 'disabled',
+ '[class.mdc-tab--active]': 'active',
+ }
+})
+export class MatTabLink extends _MatTabLinkBase implements MatInkBarItem, OnDestroy {
+ _foundation: MatInkBarFoundation;
+
+ constructor(
+ tabNavBar: MatTabNav,
+ elementRef: ElementRef,
+ @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalRippleOptions: RippleGlobalOptions|null,
+ @Attribute('tabindex') tabIndex: string, focusMonitor: FocusMonitor,
+ @Inject(DOCUMENT) _document: any,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
+ super(tabNavBar, elementRef, globalRippleOptions, tabIndex, focusMonitor, animationMode);
+ this._foundation = new MatInkBarFoundation(elementRef, _document);
+ this._foundation.init();
+ }
+
+ ngOnDestroy() {
+ super.ngOnDestroy();
+ this._foundation.destroy();
+ }
+}
diff --git a/src/material-experimental/mdc-tabs/tab.html b/src/material-experimental/mdc-tabs/tab.html
new file mode 100644
index 000000000000..99442c1f1e37
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab.html
@@ -0,0 +1,4 @@
+
+
diff --git a/src/material-experimental/mdc-tabs/tab.ts b/src/material-experimental/mdc-tabs/tab.ts
new file mode 100644
index 000000000000..c354577f808c
--- /dev/null
+++ b/src/material-experimental/mdc-tabs/tab.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import {
+ ChangeDetectionStrategy,
+ Component,
+ ViewEncapsulation,
+ TemplateRef,
+ ContentChild,
+} from '@angular/core';
+import {MatTab as BaseMatTab} from '@angular/material/tabs';
+import {MatTabContent} from './tab-content';
+import {MatTabLabel} from './tab-label';
+
+@Component({
+ moduleId: module.id,
+ selector: 'mat-tab',
+
+ // Note that usually we'd go through a bit more trouble and set up another class so that
+ // the inlined template of `MatTab` isn't duplicated, however the template is small enough
+ // that creating the extra class will generate more code than just duplicating the template.
+ templateUrl: 'tab.html',
+ inputs: ['disabled'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ exportAs: 'matTab',
+})
+export class MatTab extends BaseMatTab {
+ /**
+ * Template provided in the tab content that will be used if present, used to enable lazy-loading
+ */
+ @ContentChild(MatTabContent, {read: TemplateRef, static: true})
+ _explicitContent: TemplateRef;
+
+ /** Content for the tab label given by ``. */
+ @ContentChild(MatTabLabel, {static: false}) templateLabel: MatTabLabel;
+}
diff --git a/src/material-experimental/mdc-tabs/tabs.e2e.spec.ts b/src/material-experimental/mdc-tabs/tabs.e2e.spec.ts
index f98bacfad40b..e87348a532f4 100644
--- a/src/material-experimental/mdc-tabs/tabs.e2e.spec.ts
+++ b/src/material-experimental/mdc-tabs/tabs.e2e.spec.ts
@@ -1 +1,91 @@
-// TODO: copy tests from existing tabs, update as necessary to fix.
+import {
+ browser,
+ by,
+ element,
+ ElementArrayFinder,
+ Key,
+ ExpectedConditions
+} from 'protractor';
+import {pressKeys} from '@angular/cdk/private/testing/e2e';
+
+describe('MDC tabs', () => {
+ describe('basic behavior', () => {
+ let tabLabels: ElementArrayFinder;
+ let tabBodies: ElementArrayFinder;
+
+ beforeEach(async () => {
+ await browser.get('/mdc-tabs');
+ tabLabels = element.all(by.css('.mat-mdc-tab'));
+ tabBodies = element.all(by.css('mat-tab-body'));
+ });
+
+ it('should change tabs when the label is clicked', async () => {
+ await tabLabels.get(1).click();
+ expect(await getLabelActiveStates(tabLabels)).toEqual([false, true, false]);
+ expect(await getBodyActiveStates(tabBodies)).toEqual([false, true, false]);
+
+ await browser.wait(ExpectedConditions.not(
+ ExpectedConditions.presenceOf(element(by.css('div.mat-ripple-element')))));
+
+ await tabLabels.get(0).click();
+ expect(await getLabelActiveStates(tabLabels)).toEqual([true, false, false]);
+ expect(await getBodyActiveStates(tabBodies)).toEqual([true, false, false]);
+
+ await browser.wait(ExpectedConditions.not(
+ ExpectedConditions.presenceOf(element(by.css('div.mat-ripple-element')))));
+ });
+
+ it('should change focus with keyboard interaction', async () => {
+ const right = Key.RIGHT;
+ const left = Key.LEFT;
+
+ await tabLabels.get(0).click();
+ expect(await getFocusStates(tabLabels)).toEqual([true, false, false]);
+
+ await pressKeys(right);
+ expect(await getFocusStates(tabLabels)).toEqual([false, true, false]);
+
+ await pressKeys(right);
+ expect(await getFocusStates(tabLabels)).toEqual([false, false, true]);
+
+ await pressKeys(left);
+ expect(await getFocusStates(tabLabels)).toEqual([false, true, false]);
+
+ await pressKeys(left);
+ expect(await getFocusStates(tabLabels)).toEqual([true, false, false]);
+ });
+ });
+});
+
+/**
+ * Returns an array of true/false that represents the focus states of the provided elements.
+ */
+async function getFocusStates(elements: ElementArrayFinder) {
+ return elements.map(async el => {
+ const elementText = await el!.getText();
+ const activeText = await browser.driver.switchTo().activeElement().getText();
+
+ return activeText === elementText;
+ });
+}
+
+/** Returns an array of true/false that represents the active states for the provided elements. */
+function getLabelActiveStates(elements: ElementArrayFinder) {
+ return getClassStates(elements, 'mdc-tab--active');
+}
+
+/** Returns an array of true/false that represents the active states for the provided elements */
+function getBodyActiveStates(elements: ElementArrayFinder) {
+ return getClassStates(elements, 'mat-mdc-tab-body-active');
+}
+
+/**
+ * Returns an array of true/false values that represents whether the provided className is on
+ * each element.
+ */
+async function getClassStates(elements: ElementArrayFinder, className: string) {
+ return elements.map(async el => {
+ const classes = await el!.getAttribute('class');
+ return classes.split(/ +/g).indexOf(className) > -1;
+ });
+}
diff --git a/src/material-experimental/mdc-tabs/tabs.scss b/src/material-experimental/mdc-tabs/tabs.scss
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/src/material-experimental/mdc-tabs/tabs.spec.ts b/src/material-experimental/mdc-tabs/tabs.spec.ts
deleted file mode 100644
index f98bacfad40b..000000000000
--- a/src/material-experimental/mdc-tabs/tabs.spec.ts
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: copy tests from existing tabs, update as necessary to fix.
diff --git a/src/material/tabs/paginated-tab-header.ts b/src/material/tabs/paginated-tab-header.ts
index 8c539eee66e0..1b1200443513 100644
--- a/src/material/tabs/paginated-tab-header.ts
+++ b/src/material/tabs/paginated-tab-header.ts
@@ -25,7 +25,6 @@ import {FocusKeyManager, FocusableOption} from '@angular/cdk/a11y';
import {END, ENTER, HOME, SPACE, hasModifierKey} from '@angular/cdk/keycodes';
import {merge, of as observableOf, Subject, timer, fromEvent} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
-import {MatInkBar} from './ink-bar';
import {Platform, normalizePassiveListenerOptions} from '@angular/cdk/platform';
@@ -59,7 +58,7 @@ const HEADER_SCROLL_DELAY = 650;
const HEADER_SCROLL_INTERVAL = 100;
/** Item inside a paginated tab header. */
-type MatPaginatedTabHeaderItem = FocusableOption & {elementRef: ElementRef};
+export type MatPaginatedTabHeaderItem = FocusableOption & {elementRef: ElementRef};
/**
* Base class for a tab header that supported pagination.
@@ -67,7 +66,7 @@ type MatPaginatedTabHeaderItem = FocusableOption & {elementRef: ElementRef};
export abstract class MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit,
AfterViewInit, OnDestroy {
abstract _items: QueryList;
- abstract _inkBar: MatInkBar;
+ abstract _inkBar: {hide: () => void, alignToElement: (element: HTMLElement) => void};
abstract _tabListContainer: ElementRef;
abstract _tabList: ElementRef;
abstract _nextPaginator: ElementRef;
diff --git a/src/material/tabs/public-api.ts b/src/material/tabs/public-api.ts
index b8865e5539a7..cfa2f2a09080 100644
--- a/src/material/tabs/public-api.ts
+++ b/src/material/tabs/public-api.ts
@@ -11,15 +11,16 @@ export * from './tab-group';
export {MatInkBar, _MatInkBarPositioner, _MAT_INK_BAR_POSITIONER} from './ink-bar';
export {
MatTabBody,
+ _MatTabBodyBase,
MatTabBodyOriginState,
MatTabBodyPositionState,
MatTabBodyPortal
} from './tab-body';
-export {MatTabHeader} from './tab-header';
+export {MatTabHeader, _MatTabHeaderBase} from './tab-header';
export {MatTabLabelWrapper} from './tab-label-wrapper';
export {MatTab} from './tab';
export {MatTabLabel} from './tab-label';
-export {MatTabNav, MatTabLink} from './tab-nav-bar/index';
+export {MatTabNav, MatTabLink, _MatTabNavBase, _MatTabLinkBase} from './tab-nav-bar/index';
export {MatTabContent} from './tab-content';
export {ScrollDirection} from './paginated-tab-header';
export * from './tabs-animations';
diff --git a/src/material/tabs/tab-body.ts b/src/material/tabs/tab-body.ts
index 521f0767e9b4..4ec1bb65089c 100644
--- a/src/material/tabs/tab-body.ts
+++ b/src/material/tabs/tab-body.ts
@@ -98,24 +98,9 @@ export class MatTabBodyPortal extends CdkPortalOutlet implements OnInit, OnDestr
}
}
-/**
- * Wrapper for the contents of a tab.
- * @docs-private
- */
-@Component({
- moduleId: module.id,
- selector: 'mat-tab-body',
- templateUrl: 'tab-body.html',
- styleUrls: ['tab-body.css'],
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
- animations: [matTabsAnimations.translateTab],
- host: {
- 'class': 'mat-tab-body',
- },
-})
-export class MatTabBody implements OnInit, OnDestroy {
-
+/** Base class with all of the `MatTabBody` functionality. */
+// tslint:disable-next-line:class-name
+export abstract class _MatTabBodyBase implements OnInit, OnDestroy {
/** Current position of the tab-body in the tab-group. Zero means that the tab is visible. */
private _positionIndex: number;
@@ -141,7 +126,7 @@ export class MatTabBody implements OnInit, OnDestroy {
@Output() readonly _onCentered: EventEmitter = new EventEmitter(true);
/** The portal host inside of this container into which the tab body content will be loaded. */
- @ViewChild(PortalHostDirective, {static: false}) _portalHost: PortalHostDirective;
+ abstract _portalHost: PortalHostDirective;
/** The tab body content to display. */
@Input('content') _content: TemplatePortal;
@@ -248,3 +233,29 @@ export class MatTabBody implements OnInit, OnDestroy {
return 'right-origin-center';
}
}
+
+/**
+ * Wrapper for the contents of a tab.
+ * @docs-private
+ */
+@Component({
+ moduleId: module.id,
+ selector: 'mat-tab-body',
+ templateUrl: 'tab-body.html',
+ styleUrls: ['tab-body.css'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ animations: [matTabsAnimations.translateTab],
+ host: {
+ 'class': 'mat-tab-body',
+ }
+})
+export class MatTabBody extends _MatTabBodyBase {
+ @ViewChild(PortalHostDirective, {static: false}) _portalHost: PortalHostDirective;
+
+ constructor(elementRef: ElementRef,
+ @Optional() dir: Directionality,
+ changeDetectorRef: ChangeDetectorRef) {
+ super(elementRef, dir, changeDetectorRef);
+ }
+}
diff --git a/src/material/tabs/tab-group.ts b/src/material/tabs/tab-group.ts
index b4c0195a2ce2..38978cd0b64b 100644
--- a/src/material/tabs/tab-group.ts
+++ b/src/material/tabs/tab-group.ts
@@ -37,7 +37,6 @@ import {
} from '@angular/material/core';
import {merge, Subscription} from 'rxjs';
import {MatTab} from './tab';
-import {MatTabHeader} from './tab-header';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
@@ -66,40 +65,24 @@ export const MAT_TABS_CONFIG = new InjectionToken('MAT_TABS_CONFI
// Boilerplate for applying mixins to MatTabGroup.
/** @docs-private */
-class MatTabGroupBase {
+class MatTabGroupMixinBase {
constructor(public _elementRef: ElementRef) {}
}
-const _MatTabGroupMixinBase: CanColorCtor & CanDisableRippleCtor & typeof MatTabGroupBase =
- mixinColor(mixinDisableRipple(MatTabGroupBase), 'primary');
+const _MatTabGroupMixinBase: CanColorCtor & CanDisableRippleCtor & typeof MatTabGroupMixinBase =
+ mixinColor(mixinDisableRipple(MatTabGroupMixinBase), 'primary');
-/**
- * Material design tab-group component. Supports basic tab pairs (label + content) and includes
- * animated ink-bar, keyboard navigation, and screen reader.
- * See: https://material.io/design/components/tabs.html
- */
-@Component({
- moduleId: module.id,
- selector: 'mat-tab-group',
- exportAs: 'matTabGroup',
- templateUrl: 'tab-group.html',
- styleUrls: ['tab-group.css'],
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
- inputs: ['color', 'disableRipple'],
- host: {
- 'class': 'mat-tab-group',
- '[class.mat-tab-group-dynamic-height]': 'dynamicHeight',
- '[class.mat-tab-group-inverted-header]': 'headerPosition === "below"',
- },
-})
-export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentInit,
- AfterContentChecked, OnDestroy, CanColor, CanDisableRipple {
-
- @ContentChildren(MatTab) _tabs: QueryList;
-
- @ViewChild('tabBodyWrapper', {static: false}) _tabBodyWrapper: ElementRef;
+interface MatTabGroupBaseHeader {
+ _alignInkBarToSelectedTab: () => void;
+ focusIndex: number;
+}
- @ViewChild('tabHeader', {static: false}) _tabHeader: MatTabHeader;
+/** Base class with all of the `MatTabGroupBase` functionality. */
+// tslint:disable-next-line:class-name
+export abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements AfterContentInit,
+ AfterContentChecked, OnDestroy, CanColor, CanDisableRipple {
+ abstract _tabs: QueryList;
+ abstract _tabBodyWrapper: ElementRef;
+ abstract _tabHeader: MatTabGroupBaseHeader;
/** The tab index that should be selected after the content has been checked. */
private _indexToSelect: number | null = 0;
@@ -342,7 +325,7 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
}
/** Handle click events, setting new selected index if appropriate. */
- _handleClick(tab: MatTab, tabHeader: MatTabHeader, index: number) {
+ _handleClick(tab: MatTab, tabHeader: MatTabGroupBaseHeader, index: number) {
if (!tab.disabled) {
this.selectedIndex = tabHeader.focusIndex = index;
}
@@ -356,3 +339,36 @@ export class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentIn
return this.selectedIndex === idx ? 0 : -1;
}
}
+
+/**
+ * Material design tab-group component. Supports basic tab pairs (label + content) and includes
+ * animated ink-bar, keyboard navigation, and screen reader.
+ * See: https://material.io/design/components/tabs.html
+ */
+@Component({
+ moduleId: module.id,
+ selector: 'mat-tab-group',
+ exportAs: 'matTabGroup',
+ templateUrl: 'tab-group.html',
+ styleUrls: ['tab-group.css'],
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ inputs: ['color', 'disableRipple'],
+ host: {
+ 'class': 'mat-tab-group',
+ '[class.mat-tab-group-dynamic-height]': 'dynamicHeight',
+ '[class.mat-tab-group-inverted-header]': 'headerPosition === "below"',
+ },
+})
+export class MatTabGroup extends _MatTabGroupBase {
+ @ContentChildren(MatTab) _tabs: QueryList;
+ @ViewChild('tabBodyWrapper', {static: false}) _tabBodyWrapper: ElementRef;
+ @ViewChild('tabHeader', {static: false}) _tabHeader: MatTabGroupBaseHeader;
+
+ constructor(elementRef: ElementRef,
+ changeDetectorRef: ChangeDetectorRef,
+ @Inject(MAT_TABS_CONFIG) @Optional() defaultConfig?: MatTabsConfig,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
+ super(elementRef, changeDetectorRef, defaultConfig, animationMode);
+ }
+}
diff --git a/src/material/tabs/tab-header.ts b/src/material/tabs/tab-header.ts
index 52676efdd59f..3ea9725325e7 100644
--- a/src/material/tabs/tab-header.ts
+++ b/src/material/tabs/tab-header.ts
@@ -33,6 +33,33 @@ import {MatTabLabelWrapper} from './tab-label-wrapper';
import {Platform} from '@angular/cdk/platform';
import {MatPaginatedTabHeader} from './paginated-tab-header';
+/** Base class with all of the `MatTabHeader` functionality. */
+// tslint:disable-next-line:class-name
+export abstract class _MatTabHeaderBase extends MatPaginatedTabHeader implements
+ AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy {
+
+ /** Whether the ripple effect is disabled or not. */
+ @Input()
+ get disableRipple() { return this._disableRipple; }
+ set disableRipple(value: any) { this._disableRipple = coerceBooleanProperty(value); }
+ private _disableRipple: boolean = false;
+
+ constructor(elementRef: ElementRef,
+ changeDetectorRef: ChangeDetectorRef,
+ viewportRuler: ViewportRuler,
+ @Optional() dir: Directionality,
+ ngZone: NgZone,
+ platform: Platform,
+ // @breaking-change 9.0.0 `_animationMode` parameter to be made required.
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
+ super(elementRef, changeDetectorRef, viewportRuler, dir, ngZone, platform, animationMode);
+ }
+
+ protected _itemSelected(event: KeyboardEvent) {
+ event.preventDefault();
+ }
+}
+
/**
* The header of the tab group which displays a list of all the tabs in the tab group. Includes
* an ink bar that follows the currently selected tab. When the tabs list's width exceeds the
@@ -55,9 +82,7 @@ import {MatPaginatedTabHeader} from './paginated-tab-header';
'[class.mat-tab-header-rtl]': "_getLayoutDirection() == 'rtl'",
},
})
-export class MatTabHeader extends MatPaginatedTabHeader implements AfterContentChecked,
- AfterContentInit, AfterViewInit, OnDestroy {
-
+export class MatTabHeader extends _MatTabHeaderBase {
@ContentChildren(MatTabLabelWrapper) _items: QueryList;
@ViewChild(MatInkBar, {static: true}) _inkBar: MatInkBar;
@ViewChild('tabListContainer', {static: true}) _tabListContainer: ElementRef;
@@ -65,12 +90,6 @@ export class MatTabHeader extends MatPaginatedTabHeader implements AfterContentC
@ViewChild('nextPaginator', {static: false}) _nextPaginator: ElementRef;
@ViewChild('previousPaginator', {static: false}) _previousPaginator: ElementRef;
- /** Whether the ripple effect is disabled or not. */
- @Input()
- get disableRipple() { return this._disableRipple; }
- set disableRipple(value: any) { this._disableRipple = coerceBooleanProperty(value); }
- private _disableRipple: boolean = false;
-
constructor(elementRef: ElementRef,
changeDetectorRef: ChangeDetectorRef,
viewportRuler: ViewportRuler,
@@ -81,8 +100,4 @@ export class MatTabHeader extends MatPaginatedTabHeader implements AfterContentC
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
super(elementRef, changeDetectorRef, viewportRuler, dir, ngZone, platform, animationMode);
}
-
- protected _itemSelected(event: KeyboardEvent) {
- event.preventDefault();
- }
}
diff --git a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts b/src/material/tabs/tab-nav-bar/tab-nav-bar.ts
index a4e536b04834..7f9d89c66d17 100644
--- a/src/material/tabs/tab-nav-bar/tab-nav-bar.ts
+++ b/src/material/tabs/tab-nav-bar/tab-nav-bar.ts
@@ -45,42 +45,16 @@ import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {FocusMonitor, FocusableOption} from '@angular/cdk/a11y';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
import {MatInkBar} from '../ink-bar';
-import {MatPaginatedTabHeader} from '../paginated-tab-header';
+import {MatPaginatedTabHeader, MatPaginatedTabHeaderItem} from '../paginated-tab-header';
import {startWith, takeUntil} from 'rxjs/operators';
-
-/**
- * Navigation component matching the styles of the tab group header.
- * Provides anchored navigation with animated ink bar.
- */
-@Component({
- moduleId: module.id,
- selector: '[mat-tab-nav-bar]',
- exportAs: 'matTabNavBar, matTabNav',
- inputs: ['color'],
- templateUrl: 'tab-nav-bar.html',
- styleUrls: ['tab-nav-bar.css'],
- host: {
- 'class': 'mat-tab-nav-bar mat-tab-header',
- '[class.mat-tab-header-pagination-controls-enabled]': '_showPaginationControls',
- '[class.mat-tab-header-rtl]': "_getLayoutDirection() == 'rtl'",
- '[class.mat-primary]': 'color !== "warn" && color !== "accent"',
- '[class.mat-accent]': 'color === "accent"',
- '[class.mat-warn]': 'color === "warn"',
- },
- encapsulation: ViewEncapsulation.None,
- changeDetection: ChangeDetectionStrategy.OnPush,
-})
-export class MatTabNav extends MatPaginatedTabHeader implements AfterContentChecked,
+/** Base class with all of the `MatTabNav` functionality. */
+// tslint:disable-next-line:class-name
+export abstract class _MatTabNavBase extends MatPaginatedTabHeader implements AfterContentChecked,
AfterContentInit, OnDestroy {
/** Query list of all tab links of the tab navigation. */
- @ContentChildren(forwardRef(() => MatTabLink), {descendants: true}) _items: QueryList;
- @ViewChild(MatInkBar, {static: true}) _inkBar: MatInkBar;
- @ViewChild('tabListContainer', {static: true}) _tabListContainer: ElementRef;
- @ViewChild('tabList', {static: true}) _tabList: ElementRef;
- @ViewChild('nextPaginator', {static: false}) _nextPaginator: ElementRef;
- @ViewChild('previousPaginator', {static: false}) _previousPaginator: ElementRef;
+ abstract _items: QueryList;
/** Background color of the tab nav. */
@Input()
@@ -159,37 +133,64 @@ export class MatTabNav extends MatPaginatedTabHeader implements AfterContentChec
}
-// Boilerplate for applying mixins to MatTabLink.
-class MatTabLinkBase {}
-const _MatTabLinkMixinBase:
- HasTabIndexCtor & CanDisableRippleCtor & CanDisableCtor & typeof MatTabLinkBase =
- mixinTabIndex(mixinDisableRipple(mixinDisabled(MatTabLinkBase)));
-
/**
- * Link inside of a `mat-tab-nav-bar`.
+ * Navigation component matching the styles of the tab group header.
+ * Provides anchored navigation with animated ink bar.
*/
-@Directive({
- selector: '[mat-tab-link], [matTabLink]',
- exportAs: 'matTabLink',
- inputs: ['disabled', 'disableRipple', 'tabIndex'],
+@Component({
+ moduleId: module.id,
+ selector: '[mat-tab-nav-bar]',
+ exportAs: 'matTabNavBar, matTabNav',
+ inputs: ['color'],
+ templateUrl: 'tab-nav-bar.html',
+ styleUrls: ['tab-nav-bar.css'],
host: {
- 'class': 'mat-tab-link',
- '[attr.aria-current]': 'active ? "page" : null',
- '[attr.aria-disabled]': 'disabled',
- '[attr.tabIndex]': 'tabIndex',
- '[class.mat-tab-disabled]': 'disabled',
- '[class.mat-tab-label-active]': 'active',
- }
+ 'class': 'mat-tab-nav-bar mat-tab-header',
+ '[class.mat-tab-header-pagination-controls-enabled]': '_showPaginationControls',
+ '[class.mat-tab-header-rtl]': "_getLayoutDirection() == 'rtl'",
+ '[class.mat-primary]': 'color !== "warn" && color !== "accent"',
+ '[class.mat-accent]': 'color === "accent"',
+ '[class.mat-warn]': 'color === "warn"',
+ },
+ encapsulation: ViewEncapsulation.None,
+ changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MatTabLink extends _MatTabLinkMixinBase implements OnDestroy, CanDisable,
+export class MatTabNav extends _MatTabNavBase {
+ @ContentChildren(forwardRef(() => MatTabLink), {descendants: true}) _items: QueryList;
+ @ViewChild(MatInkBar, {static: true}) _inkBar: MatInkBar;
+ @ViewChild('tabListContainer', {static: true}) _tabListContainer: ElementRef;
+ @ViewChild('tabList', {static: true}) _tabList: ElementRef;
+ @ViewChild('nextPaginator', {static: false}) _nextPaginator: ElementRef;
+ @ViewChild('previousPaginator', {static: false}) _previousPaginator: ElementRef;
+
+ constructor(elementRef: ElementRef,
+ @Optional() dir: Directionality,
+ ngZone: NgZone,
+ changeDetectorRef: ChangeDetectorRef,
+ viewportRuler: ViewportRuler,
+ /**
+ * @deprecated @breaking-change 9.0.0 `platform` parameter to become required.
+ */
+ @Optional() platform?: Platform,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
+ super(elementRef, dir, ngZone, changeDetectorRef, viewportRuler, platform, animationMode);
+ }
+}
+
+// Boilerplate for applying mixins to MatTabLink.
+class MatTabLinkMixinBase {}
+const _MatTabLinkMixinBase:
+ HasTabIndexCtor & CanDisableRippleCtor & CanDisableCtor & typeof MatTabLinkMixinBase =
+ mixinTabIndex(mixinDisableRipple(mixinDisabled(MatTabLinkMixinBase)));
+
+/** Base class with all of the `MatTabLink` functionality. */
+// tslint:disable-next-line:class-name
+export class _MatTabLinkBase extends _MatTabLinkMixinBase implements OnDestroy, CanDisable,
CanDisableRipple, HasTabIndex, RippleTarget, FocusableOption {
/** Whether the tab link is active or not. */
protected _isActive: boolean = false;
- /** Reference to the RippleRenderer for the tab-link. */
- protected _tabLinkRipple: RippleRenderer;
-
/** Whether the link is active. */
@Input()
get active(): boolean { return this._isActive; }
@@ -218,15 +219,12 @@ export class MatTabLink extends _MatTabLinkMixinBase implements OnDestroy, CanDi
}
constructor(
- private _tabNavBar: MatTabNav, public elementRef: ElementRef, ngZone: NgZone,
- platform: Platform,
+ private _tabNavBar: _MatTabNavBase, public elementRef: ElementRef,
@Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalRippleOptions: RippleGlobalOptions|null,
@Attribute('tabindex') tabIndex: string, private _focusMonitor: FocusMonitor,
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
super();
- this._tabLinkRipple = new RippleRenderer(this, ngZone, elementRef, platform);
- this._tabLinkRipple.setupTriggerEvents(elementRef);
this.rippleConfig = globalRippleOptions || {};
this.tabIndex = parseInt(tabIndex) || 0;
@@ -242,7 +240,44 @@ export class MatTabLink extends _MatTabLinkMixinBase implements OnDestroy, CanDi
}
ngOnDestroy() {
- this._tabLinkRipple._removeTriggerEvents();
this._focusMonitor.stopMonitoring(this.elementRef);
}
}
+
+
+/**
+ * Link inside of a `mat-tab-nav-bar`.
+ */
+@Directive({
+ selector: '[mat-tab-link], [matTabLink]',
+ exportAs: 'matTabLink',
+ inputs: ['disabled', 'disableRipple', 'tabIndex'],
+ host: {
+ 'class': 'mat-tab-link',
+ '[attr.aria-current]': 'active ? "page" : null',
+ '[attr.aria-disabled]': 'disabled',
+ '[attr.tabIndex]': 'tabIndex',
+ '[class.mat-tab-disabled]': 'disabled',
+ '[class.mat-tab-label-active]': 'active',
+ }
+})
+export class MatTabLink extends _MatTabLinkBase implements OnDestroy {
+ /** Reference to the RippleRenderer for the tab-link. */
+ private _tabLinkRipple: RippleRenderer;
+
+ constructor(
+ tabNavBar: MatTabNav, elementRef: ElementRef, ngZone: NgZone,
+ platform: Platform,
+ @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) globalRippleOptions: RippleGlobalOptions|null,
+ @Attribute('tabindex') tabIndex: string, focusMonitor: FocusMonitor,
+ @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
+ super(tabNavBar, elementRef, globalRippleOptions, tabIndex, focusMonitor, animationMode);
+ this._tabLinkRipple = new RippleRenderer(this, ngZone, elementRef, platform);
+ this._tabLinkRipple.setupTriggerEvents(elementRef.nativeElement);
+ }
+
+ ngOnDestroy() {
+ super.ngOnDestroy();
+ this._tabLinkRipple._removeTriggerEvents();
+ }
+}
diff --git a/tools/public_api_guard/material/tabs.d.ts b/tools/public_api_guard/material/tabs.d.ts
index 02dc5e54594e..2652c67ff79e 100644
--- a/tools/public_api_guard/material/tabs.d.ts
+++ b/tools/public_api_guard/material/tabs.d.ts
@@ -7,6 +7,85 @@ export interface _MatInkBarPositioner {
};
}
+export declare abstract class _MatTabBodyBase implements OnInit, OnDestroy {
+ readonly _afterLeavingCenter: EventEmitter;
+ readonly _beforeCentering: EventEmitter;
+ _content: TemplatePortal;
+ readonly _onCentered: EventEmitter;
+ readonly _onCentering: EventEmitter;
+ abstract _portalHost: PortalHostDirective;
+ _position: MatTabBodyPositionState;
+ _translateTabComplete: Subject;
+ animationDuration: string;
+ origin: number;
+ position: number;
+ constructor(_elementRef: ElementRef, _dir: Directionality, changeDetectorRef: ChangeDetectorRef);
+ _getLayoutDirection(): Direction;
+ _isCenterPosition(position: MatTabBodyPositionState | string): boolean;
+ _onTranslateTabStarted(event: AnimationEvent): void;
+ ngOnDestroy(): void;
+ ngOnInit(): void;
+}
+
+export declare abstract class _MatTabGroupBase extends _MatTabGroupMixinBase implements AfterContentInit, AfterContentChecked, OnDestroy, CanColor, CanDisableRipple {
+ _animationMode?: string | undefined;
+ abstract _tabBodyWrapper: ElementRef;
+ abstract _tabHeader: MatTabGroupBaseHeader;
+ abstract _tabs: QueryList;
+ readonly animationDone: EventEmitter;
+ animationDuration: string;
+ backgroundColor: ThemePalette;
+ dynamicHeight: boolean;
+ readonly focusChange: EventEmitter;
+ headerPosition: MatTabHeaderPosition;
+ selectedIndex: number | null;
+ readonly selectedIndexChange: EventEmitter;
+ readonly selectedTabChange: EventEmitter;
+ constructor(elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, defaultConfig?: MatTabsConfig, _animationMode?: string | undefined);
+ _focusChanged(index: number): void;
+ _getTabContentId(i: number): string;
+ _getTabIndex(tab: MatTab, idx: number): number | null;
+ _getTabLabelId(i: number): string;
+ _handleClick(tab: MatTab, tabHeader: MatTabGroupBaseHeader, index: number): void;
+ _removeTabBodyWrapperHeight(): void;
+ _setTabBodyWrapperHeight(tabHeight: number): void;
+ ngAfterContentChecked(): void;
+ ngAfterContentInit(): void;
+ ngOnDestroy(): void;
+ realignInkBar(): void;
+}
+
+export declare abstract class _MatTabHeaderBase extends MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy {
+ disableRipple: any;
+ constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, viewportRuler: ViewportRuler, dir: Directionality, ngZone: NgZone, platform: Platform, animationMode?: string);
+ protected _itemSelected(event: KeyboardEvent): void;
+}
+
+export declare class _MatTabLinkBase extends _MatTabLinkMixinBase implements OnDestroy, CanDisable, CanDisableRipple, HasTabIndex, RippleTarget, FocusableOption {
+ protected _isActive: boolean;
+ active: boolean;
+ elementRef: ElementRef;
+ rippleConfig: RippleConfig & RippleGlobalOptions;
+ readonly rippleDisabled: boolean;
+ constructor(_tabNavBar: _MatTabNavBase, elementRef: ElementRef, globalRippleOptions: RippleGlobalOptions | null, tabIndex: string, _focusMonitor: FocusMonitor, animationMode?: string);
+ focus(): void;
+ ngOnDestroy(): void;
+}
+
+export declare abstract class _MatTabNavBase extends MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit, OnDestroy {
+ abstract _items: QueryList;
+ backgroundColor: ThemePalette;
+ color: ThemePalette;
+ disableRipple: any;
+ constructor(elementRef: ElementRef, dir: Directionality, ngZone: NgZone, changeDetectorRef: ChangeDetectorRef, viewportRuler: ViewportRuler,
+ platform?: Platform, animationMode?: string);
+ protected _itemSelected(): void;
+ ngAfterContentInit(): void;
+ updateActiveLink(_element?: ElementRef): void;
+}
+
export declare const MAT_TABS_CONFIG: InjectionToken;
export declare class MatInkBar {
@@ -35,24 +114,9 @@ export declare class MatTab extends _MatTabMixinBase implements OnInit, CanDisab
ngOnInit(): void;
}
-export declare class MatTabBody implements OnInit, OnDestroy {
- readonly _afterLeavingCenter: EventEmitter;
- readonly _beforeCentering: EventEmitter;
- _content: TemplatePortal;
- readonly _onCentered: EventEmitter;
- readonly _onCentering: EventEmitter;
+export declare class MatTabBody extends _MatTabBodyBase {
_portalHost: PortalHostDirective;
- _position: MatTabBodyPositionState;
- _translateTabComplete: Subject;
- animationDuration: string;
- origin: number;
- position: number;
- constructor(_elementRef: ElementRef, _dir: Directionality, changeDetectorRef: ChangeDetectorRef);
- _getLayoutDirection(): Direction;
- _isCenterPosition(position: MatTabBodyPositionState | string): boolean;
- _onTranslateTabStarted(event: AnimationEvent): void;
- ngOnDestroy(): void;
- ngOnInit(): void;
+ constructor(elementRef: ElementRef, dir: Directionality, changeDetectorRef: ChangeDetectorRef);
}
export declare type MatTabBodyOriginState = 'left' | 'right';
@@ -75,44 +139,21 @@ export declare class MatTabContent {
constructor(template: TemplateRef);
}
-export declare class MatTabGroup extends _MatTabGroupMixinBase implements AfterContentInit, AfterContentChecked, OnDestroy, CanColor, CanDisableRipple {
- _animationMode?: string | undefined;
+export declare class MatTabGroup extends _MatTabGroupBase {
_tabBodyWrapper: ElementRef;
- _tabHeader: MatTabHeader;
+ _tabHeader: MatTabGroupBaseHeader;
_tabs: QueryList;
- readonly animationDone: EventEmitter;
- animationDuration: string;
- backgroundColor: ThemePalette;
- dynamicHeight: boolean;
- readonly focusChange: EventEmitter;
- headerPosition: MatTabHeaderPosition;
- selectedIndex: number | null;
- readonly selectedIndexChange: EventEmitter;
- readonly selectedTabChange: EventEmitter;
- constructor(elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, defaultConfig?: MatTabsConfig, _animationMode?: string | undefined);
- _focusChanged(index: number): void;
- _getTabContentId(i: number): string;
- _getTabIndex(tab: MatTab, idx: number): number | null;
- _getTabLabelId(i: number): string;
- _handleClick(tab: MatTab, tabHeader: MatTabHeader, index: number): void;
- _removeTabBodyWrapperHeight(): void;
- _setTabBodyWrapperHeight(tabHeight: number): void;
- ngAfterContentChecked(): void;
- ngAfterContentInit(): void;
- ngOnDestroy(): void;
- realignInkBar(): void;
+ constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, defaultConfig?: MatTabsConfig, animationMode?: string);
}
-export declare class MatTabHeader extends MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy {
+export declare class MatTabHeader extends _MatTabHeaderBase {
_inkBar: MatInkBar;
_items: QueryList;
_nextPaginator: ElementRef;
_previousPaginator: ElementRef;
_tabList: ElementRef;
_tabListContainer: ElementRef;
- disableRipple: any;
constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, viewportRuler: ViewportRuler, dir: Directionality, ngZone: NgZone, platform: Platform, animationMode?: string);
- protected _itemSelected(event: KeyboardEvent): void;
}
export declare type MatTabHeaderPosition = 'above' | 'below';
@@ -128,33 +169,20 @@ export declare class MatTabLabelWrapper extends _MatTabLabelWrapperMixinBase imp
getOffsetWidth(): number;
}
-export declare class MatTabLink extends _MatTabLinkMixinBase implements OnDestroy, CanDisable, CanDisableRipple, HasTabIndex, RippleTarget, FocusableOption {
- protected _isActive: boolean;
- protected _tabLinkRipple: RippleRenderer;
- active: boolean;
- elementRef: ElementRef;
- rippleConfig: RippleConfig & RippleGlobalOptions;
- readonly rippleDisabled: boolean;
- constructor(_tabNavBar: MatTabNav, elementRef: ElementRef, ngZone: NgZone, platform: Platform, globalRippleOptions: RippleGlobalOptions | null, tabIndex: string, _focusMonitor: FocusMonitor, animationMode?: string);
- focus(): void;
+export declare class MatTabLink extends _MatTabLinkBase implements OnDestroy {
+ constructor(tabNavBar: MatTabNav, elementRef: ElementRef, ngZone: NgZone, platform: Platform, globalRippleOptions: RippleGlobalOptions | null, tabIndex: string, focusMonitor: FocusMonitor, animationMode?: string);
ngOnDestroy(): void;
}
-export declare class MatTabNav extends MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit, OnDestroy {
+export declare class MatTabNav extends _MatTabNavBase {
_inkBar: MatInkBar;
_items: QueryList;
_nextPaginator: ElementRef;
_previousPaginator: ElementRef;
_tabList: ElementRef;
_tabListContainer: ElementRef;
- backgroundColor: ThemePalette;
- color: ThemePalette;
- disableRipple: any;
constructor(elementRef: ElementRef, dir: Directionality, ngZone: NgZone, changeDetectorRef: ChangeDetectorRef, viewportRuler: ViewportRuler,
platform?: Platform, animationMode?: string);
- protected _itemSelected(): void;
- ngAfterContentInit(): void;
- updateActiveLink(_element?: ElementRef): void;
}
export declare const matTabsAnimations: {