Skip to content

Commit ea78a47

Browse files
authoredJan 7, 2022
feat(material/tabs): Refactor MatTabNav to follow the ARIA tabs pattern (#24062)
by introducing a new tabpanel component.
1 parent 1dd2955 commit ea78a47

23 files changed

+618
-18
lines changed
 

‎src/components-examples/material/tabs/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {TabGroupLazyLoadedExample} from './tab-group-lazy-loaded/tab-group-lazy-
2020
import {TabGroupStretchedExample} from './tab-group-stretched/tab-group-stretched-example';
2121
import {TabGroupThemeExample} from './tab-group-theme/tab-group-theme-example';
2222
import {TabNavBarBasicExample} from './tab-nav-bar-basic/tab-nav-bar-basic-example';
23+
import {TabNavBarWithPanelExample} from './tab-nav-bar-with-panel/tab-nav-bar-with-panel-example';
2324

2425
export {
2526
TabGroupAlignExample,
@@ -35,6 +36,7 @@ export {
3536
TabGroupStretchedExample,
3637
TabGroupThemeExample,
3738
TabNavBarBasicExample,
39+
TabNavBarWithPanelExample,
3840
};
3941

4042
const EXAMPLES = [
@@ -51,6 +53,7 @@ const EXAMPLES = [
5153
TabGroupStretchedExample,
5254
TabGroupThemeExample,
5355
TabNavBarBasicExample,
56+
TabNavBarWithPanelExample,
5457
];
5558

5659
@NgModule({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.example-action-button {
2+
margin-top: 8px;
3+
margin-right: 8px;
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!-- #docregion mat-tab-nav -->
2+
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
3+
<a mat-tab-link *ngFor="let link of links"
4+
(click)="activeLink = link"
5+
[active]="activeLink == link"> {{link}} </a>
6+
<a mat-tab-link disabled>Disabled Link</a>
7+
</nav>
8+
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
9+
<!-- #enddocregion mat-tab-nav -->
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {Component} from '@angular/core';
2+
3+
/**
4+
* @title Use of the tab nav bar with the dedicated panel component.
5+
*/
6+
@Component({
7+
selector: 'tab-nav-bar-with-panel-example',
8+
templateUrl: 'tab-nav-bar-with-panel-example.html',
9+
styleUrls: ['tab-nav-bar-with-panel-example.css'],
10+
})
11+
export class TabNavBarWithPanelExample {
12+
links = ['First', 'Second', 'Third'];
13+
activeLink = this.links[0];
14+
}

‎src/dev-app/mdc-tabs/mdc-tabs-demo.html

+9
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,13 @@ <h2>Tab nav bar</h2>
127127
[active]="activeLink == link">{{link}}</a>
128128
<a mat-tab-link disabled>Disabled Link</a>
129129
</nav>
130+
131+
<h2>Tab nav bar with panel</h2>
132+
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
133+
<a mat-tab-link *ngFor="let link of links"
134+
(click)="activeLink = link"
135+
[active]="activeLink == link">{{link}}</a>
136+
<a mat-tab-link disabled>Disabled Link</a>
137+
</nav>
138+
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
130139
</div>

‎src/dev-app/tabs/tabs-demo.html

+3-1
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,7 @@ <h3>Tab group stretched</h3>
1818
<tab-group-stretched-example></tab-group-stretched-example>
1919
<h3>Tab group theming</h3>
2020
<tab-group-theme-example></tab-group-theme-example>
21-
<h3>Tab Navigation Bar basic</h3>
21+
<h3>Tab navigation bar basic</h3>
2222
<tab-nav-bar-basic-example></tab-nav-bar-basic-example>
23+
<h3>Tab navigation bar with panel</h3>
24+
<tab-nav-bar-with-panel-example></tab-nav-bar-with-panel-example>

‎src/material-experimental/mdc-tabs/module.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {MatTabLabelWrapper} from './tab-label-wrapper';
1919
import {MatTab} from './tab';
2020
import {MatTabHeader} from './tab-header';
2121
import {MatTabGroup} from './tab-group';
22-
import {MatTabNav, MatTabLink} from './tab-nav-bar/tab-nav-bar';
22+
import {MatTabNav, MatTabNavPanel, MatTabLink} from './tab-nav-bar/tab-nav-bar';
2323

2424
@NgModule({
2525
imports: [
@@ -37,6 +37,7 @@ import {MatTabNav, MatTabLink} from './tab-nav-bar/tab-nav-bar';
3737
MatTab,
3838
MatTabGroup,
3939
MatTabNav,
40+
MatTabNavPanel,
4041
MatTabLink,
4142
],
4243
declarations: [
@@ -45,6 +46,7 @@ import {MatTabNav, MatTabLink} from './tab-nav-bar/tab-nav-bar';
4546
MatTab,
4647
MatTabGroup,
4748
MatTabNav,
49+
MatTabNavPanel,
4850
MatTabLink,
4951

5052
// Private directives, should not be exported.

‎src/material-experimental/mdc-tabs/public-api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export {MatTab} from './tab';
1515
export {MatInkBar} from './ink-bar';
1616
export {MatTabHeader} from './tab-header';
1717
export {MatTabGroup} from './tab-group';
18-
export {MatTabNav, MatTabLink} from './tab-nav-bar/tab-nav-bar';
18+
export {MatTabNav, MatTabNavPanel, MatTabLink} from './tab-nav-bar/tab-nav-bar';
1919

2020
export {
2121
MatTabBodyPositionState,

‎src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.spec.ts

+142-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1+
import {SPACE} from '@angular/cdk/keycodes';
12
import {waitForAsync, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
23
import {Component, QueryList, ViewChild, ViewChildren} from '@angular/core';
34
import {
45
MAT_RIPPLE_GLOBAL_OPTIONS,
56
RippleGlobalOptions,
67
} from '@angular/material-experimental/mdc-core';
78
import {By} from '@angular/platform-browser';
8-
import {dispatchFakeEvent, dispatchMouseEvent} from '../../../cdk/testing/private';
9+
import {
10+
dispatchFakeEvent,
11+
dispatchKeyboardEvent,
12+
dispatchMouseEvent,
13+
} from '../../../cdk/testing/private';
914
import {Direction, Directionality} from '@angular/cdk/bidi';
1015
import {Subject} from 'rxjs';
1116
import {MatTabsModule} from '../module';
@@ -30,6 +35,7 @@ describe('MDC-based MatTabNavBar', () => {
3035
TabLinkWithTabIndexBinding,
3136
TabLinkWithNativeTabindexAttr,
3237
TabBarWithInactiveTabsOnInit,
38+
TabBarWithPanel,
3339
],
3440
providers: [
3541
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions},
@@ -309,6 +315,123 @@ describe('MDC-based MatTabNavBar', () => {
309315
expect(instance.tabNavBar.selectedIndex).toBe(1);
310316
});
311317

318+
describe('without panel', () => {
319+
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;
320+
321+
beforeEach(() => {
322+
fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
323+
fixture.detectChanges();
324+
});
325+
326+
it('should have no explicit roles', () => {
327+
const tabBar = fixture.nativeElement.querySelector('.mat-mdc-tab-nav-bar')!;
328+
expect(tabBar.getAttribute('role')).toBe(null);
329+
330+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
331+
expect(tabLinks[0].getAttribute('role')).toBe(null);
332+
expect(tabLinks[1].getAttribute('role')).toBe(null);
333+
expect(tabLinks[2].getAttribute('role')).toBe(null);
334+
});
335+
336+
it('should not setup aria-controls', () => {
337+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
338+
expect(tabLinks[0].getAttribute('aria-controls')).toBe(null);
339+
expect(tabLinks[1].getAttribute('aria-controls')).toBe(null);
340+
expect(tabLinks[2].getAttribute('aria-controls')).toBe(null);
341+
});
342+
343+
it('should not manage aria-selected', () => {
344+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
345+
expect(tabLinks[0].getAttribute('aria-selected')).toBe(null);
346+
expect(tabLinks[1].getAttribute('aria-selected')).toBe(null);
347+
expect(tabLinks[2].getAttribute('aria-selected')).toBe(null);
348+
});
349+
350+
it('should not activate a link when space is pressed', () => {
351+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
352+
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(false);
353+
354+
dispatchKeyboardEvent(tabLinks[1], 'keydown', SPACE);
355+
fixture.detectChanges();
356+
357+
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(false);
358+
});
359+
});
360+
361+
describe('with panel', () => {
362+
let fixture: ComponentFixture<TabBarWithPanel>;
363+
364+
beforeEach(() => {
365+
fixture = TestBed.createComponent(TabBarWithPanel);
366+
fixture.detectChanges();
367+
});
368+
369+
it('should have the proper roles', () => {
370+
const tabBar = fixture.nativeElement.querySelector('.mat-mdc-tab-nav-bar')!;
371+
expect(tabBar.getAttribute('role')).toBe('tablist');
372+
373+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
374+
expect(tabLinks[0].getAttribute('role')).toBe('tab');
375+
expect(tabLinks[1].getAttribute('role')).toBe('tab');
376+
expect(tabLinks[2].getAttribute('role')).toBe('tab');
377+
378+
const tabPanel = fixture.nativeElement.querySelector('.mat-mdc-tab-nav-panel')!;
379+
expect(tabPanel.getAttribute('role')).toBe('tabpanel');
380+
});
381+
382+
it('should manage tabindex properly', () => {
383+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
384+
expect(tabLinks[0].tabIndex).toBe(0);
385+
expect(tabLinks[1].tabIndex).toBe(-1);
386+
expect(tabLinks[2].tabIndex).toBe(-1);
387+
388+
tabLinks[1].click();
389+
fixture.detectChanges();
390+
391+
expect(tabLinks[0].tabIndex).toBe(-1);
392+
expect(tabLinks[1].tabIndex).toBe(0);
393+
expect(tabLinks[2].tabIndex).toBe(-1);
394+
});
395+
396+
it('should setup aria-controls properly', () => {
397+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
398+
expect(tabLinks[0].getAttribute('aria-controls')).toBe('tab-panel');
399+
expect(tabLinks[1].getAttribute('aria-controls')).toBe('tab-panel');
400+
expect(tabLinks[2].getAttribute('aria-controls')).toBe('tab-panel');
401+
});
402+
403+
it('should not manage aria-current', () => {
404+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
405+
expect(tabLinks[0].getAttribute('aria-current')).toBe(null);
406+
expect(tabLinks[1].getAttribute('aria-current')).toBe(null);
407+
expect(tabLinks[2].getAttribute('aria-current')).toBe(null);
408+
});
409+
410+
it('should manage aria-selected properly', () => {
411+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
412+
expect(tabLinks[0].getAttribute('aria-selected')).toBe('true');
413+
expect(tabLinks[1].getAttribute('aria-selected')).toBe('false');
414+
expect(tabLinks[2].getAttribute('aria-selected')).toBe('false');
415+
416+
tabLinks[1].click();
417+
fixture.detectChanges();
418+
419+
expect(tabLinks[0].getAttribute('aria-selected')).toBe('false');
420+
expect(tabLinks[1].getAttribute('aria-selected')).toBe('true');
421+
expect(tabLinks[2].getAttribute('aria-selected')).toBe('false');
422+
});
423+
424+
it('should activate a link when space is pressed', () => {
425+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-mdc-tab-link');
426+
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(false);
427+
428+
dispatchKeyboardEvent(tabLinks[1], 'keydown', SPACE);
429+
fixture.detectChanges();
430+
431+
expect(tabLinks[1].classList.contains('mdc-tab--active')).toBe(true);
432+
});
433+
});
434+
312435
describe('ripples', () => {
313436
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;
314437

@@ -532,3 +655,21 @@ class TabLinkWithNativeTabindexAttr {}
532655
class TabBarWithInactiveTabsOnInit {
533656
tabs = [0, 1, 2];
534657
}
658+
659+
@Component({
660+
template: `
661+
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
662+
<a mat-tab-link
663+
*ngFor="let tab of tabs; let index = index"
664+
[active]="index === activeIndex"
665+
(click)="activeIndex = index">
666+
Tab link
667+
</a>
668+
</nav>
669+
<mat-tab-nav-panel #tabPanel id="tab-panel">Tab panel</mat-tab-nav-panel>
670+
`,
671+
})
672+
class TabBarWithPanel {
673+
tabs = [0, 1, 2];
674+
activeIndex = 0;
675+
}

‎src/material-experimental/mdc-tabs/tab-nav-bar/tab-nav-bar.ts

+35-2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {takeUntil} from 'rxjs/operators';
5656
templateUrl: 'tab-nav-bar.html',
5757
styleUrls: ['tab-nav-bar.css'],
5858
host: {
59+
'[attr.role]': '_getRole()',
5960
'class': 'mat-mdc-tab-nav-bar mat-mdc-tab-header',
6061
'[class.mat-mdc-tab-header-pagination-controls-enabled]': '_showPaginationControls',
6162
'[class.mat-mdc-tab-header-rtl]': "_getLayoutDirection() == 'rtl'",
@@ -127,12 +128,17 @@ export class MatTabNav extends _MatTabNavBase implements AfterContentInit {
127128
styleUrls: ['tab-link.css'],
128129
host: {
129130
'class': 'mdc-tab mat-mdc-tab-link mat-mdc-focus-indicator',
130-
'[attr.aria-current]': 'active ? "page" : null',
131+
'[attr.aria-controls]': '_getAriaControls()',
132+
'[attr.aria-current]': '_getAriaCurrent()',
131133
'[attr.aria-disabled]': 'disabled',
132-
'[attr.tabIndex]': 'tabIndex',
134+
'[attr.aria-selected]': '_getAriaSelected()',
135+
'[attr.id]': 'id',
136+
'[attr.tabIndex]': '_getTabIndex()',
137+
'[attr.role]': '_getRole()',
133138
'[class.mat-mdc-tab-disabled]': 'disabled',
134139
'[class.mdc-tab--active]': 'active',
135140
'(focus)': '_handleFocus()',
141+
'(keydown)': '_handleKeydown($event)',
136142
},
137143
})
138144
export class MatTabLink extends _MatTabLinkBase implements MatInkBarItem, OnInit, OnDestroy {
@@ -167,3 +173,30 @@ export class MatTabLink extends _MatTabLinkBase implements MatInkBarItem, OnInit
167173
this._foundation.destroy();
168174
}
169175
}
176+
177+
// Increasing integer for generating unique ids for tab nav components.
178+
let nextUniqueId = 0;
179+
180+
/**
181+
* Tab panel component associated with MatTabNav.
182+
*/
183+
@Component({
184+
selector: 'mat-tab-nav-panel',
185+
exportAs: 'matTabNavPanel',
186+
template: '<ng-content></ng-content>',
187+
host: {
188+
'[attr.aria-labelledby]': '_activeTabId',
189+
'[attr.id]': 'id',
190+
'class': 'mat-mdc-tab-nav-panel',
191+
'role': 'tabpanel',
192+
},
193+
encapsulation: ViewEncapsulation.None,
194+
changeDetection: ChangeDetectionStrategy.OnPush,
195+
})
196+
export class MatTabNavPanel {
197+
/** Unique id for the tab panel. */
198+
@Input() id = `mat-tab-nav-panel-${nextUniqueId++}`;
199+
200+
/** Id of the active tab in the nav bar. */
201+
_activeTabId?: string;
202+
}

‎src/material-experimental/mdc-tabs/testing/tab-harness-filters.ts

+3
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ export interface TabLinkHarnessFilters extends BaseHarnessFilters {
2727

2828
/** A set of criteria that can be used to filter a list of `MatTabNavBarHarness` instances. */
2929
export interface TabNavBarHarnessFilters extends BaseHarnessFilters {}
30+
31+
/** A set of criteria that can be used to filter a list of `MatTabNavBarHarness` instances. */
32+
export interface TabNavPanelHarnessFilters extends BaseHarnessFilters {}

‎src/material-experimental/mdc-tabs/testing/tab-nav-bar-harness.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
*/
88

99
import {ComponentHarness, HarnessPredicate, parallel} from '@angular/cdk/testing';
10-
import {TabNavBarHarnessFilters, TabLinkHarnessFilters} from './tab-harness-filters';
10+
import {
11+
TabNavBarHarnessFilters,
12+
TabNavPanelHarnessFilters,
13+
TabLinkHarnessFilters,
14+
} from './tab-harness-filters';
1115
import {MatTabLinkHarness} from './tab-link-harness';
16+
import {MatTabNavPanelHarness} from './tab-nav-panel-harness';
1217

1318
/** Harness for interacting with an MDC-based mat-tab-nav-bar in tests. */
1419
export class MatTabNavBarHarness extends ComponentHarness {
@@ -57,4 +62,17 @@ export class MatTabNavBarHarness extends ComponentHarness {
5762
}
5863
await tabs[0].click();
5964
}
65+
66+
/** Gets the panel associated with the nav bar. */
67+
async getPanel(): Promise<MatTabNavPanelHarness> {
68+
const link = await this.getActiveLink();
69+
const host = await link.host();
70+
const panelId = await host.getAttribute('aria-controls');
71+
if (!panelId) {
72+
throw Error('No panel is controlled by the nav bar.');
73+
}
74+
75+
const filter: TabNavPanelHarnessFilters = {selector: `#${panelId}`};
76+
return await this.documentRootLocatorFactory().locatorFor(MatTabNavPanelHarness.with(filter))();
77+
}
6078
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ContentContainerComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
10+
import {TabNavPanelHarnessFilters} from './tab-harness-filters';
11+
12+
/** Harness for interacting with a standard mat-tab-nav-panel in tests. */
13+
export class MatTabNavPanelHarness extends ContentContainerComponentHarness {
14+
/** The selector for the host element of a `MatTabNavPanel` instance. */
15+
static hostSelector = '.mat-mdc-tab-nav-panel';
16+
17+
/**
18+
* Gets a `HarnessPredicate` that can be used to search for a `MatTabNavPanel` that meets
19+
* certain criteria.
20+
* @param options Options for filtering which tab nav panel instances are considered a match.
21+
* @return a `HarnessPredicate` configured with the given options.
22+
*/
23+
static with(options: TabNavPanelHarnessFilters = {}): HarnessPredicate<MatTabNavPanelHarness> {
24+
return new HarnessPredicate(MatTabNavPanelHarness, options);
25+
}
26+
27+
/** Gets the tab panel text content. */
28+
async getTextContent(): Promise<string> {
29+
return (await this.host()).text();
30+
}
31+
}

‎src/material/tabs/public-api.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ export {MatTabHeader, _MatTabHeaderBase} from './tab-header';
2020
export {MatTabLabelWrapper} from './tab-label-wrapper';
2121
export {MatTab, MAT_TAB_GROUP} from './tab';
2222
export {MatTabLabel, MAT_TAB} from './tab-label';
23-
export {MatTabNav, MatTabLink, _MatTabNavBase, _MatTabLinkBase} from './tab-nav-bar/index';
23+
export {
24+
MatTabNav,
25+
MatTabLink,
26+
MatTabNavPanel,
27+
_MatTabNavBase,
28+
_MatTabLinkBase,
29+
} from './tab-nav-bar/index';
2430
export {MatTabContent} from './tab-content';
2531
export {ScrollDirection} from './paginated-tab-header';
2632
export * from './tabs-animations';

‎src/material/tabs/tab-nav-bar/tab-nav-bar.spec.ts

+142-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
import {SPACE} from '@angular/cdk/keycodes';
12
import {waitForAsync, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
23
import {Component, ViewChild, ViewChildren, QueryList} from '@angular/core';
34
import {MAT_RIPPLE_GLOBAL_OPTIONS, RippleGlobalOptions} from '@angular/material/core';
45
import {By} from '@angular/platform-browser';
5-
import {dispatchFakeEvent, dispatchMouseEvent} from '../../../cdk/testing/private';
6+
import {
7+
dispatchFakeEvent,
8+
dispatchKeyboardEvent,
9+
dispatchMouseEvent,
10+
} from '../../../cdk/testing/private';
611
import {Direction, Directionality} from '@angular/cdk/bidi';
712
import {Subject} from 'rxjs';
813
import {MatTabLink, MatTabNav, MatTabsModule} from '../index';
@@ -24,6 +29,7 @@ describe('MatTabNavBar', () => {
2429
TabLinkWithTabIndexBinding,
2530
TabLinkWithNativeTabindexAttr,
2631
TabBarWithInactiveTabsOnInit,
32+
TabBarWithPanel,
2733
],
2834
providers: [
2935
{provide: MAT_RIPPLE_GLOBAL_OPTIONS, useFactory: () => globalRippleOptions},
@@ -295,6 +301,123 @@ describe('MatTabNavBar', () => {
295301
expect(instance.tabNavBar.selectedIndex).toBe(1);
296302
});
297303

304+
describe('without panel', () => {
305+
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;
306+
307+
beforeEach(() => {
308+
fixture = TestBed.createComponent(SimpleTabNavBarTestApp);
309+
fixture.detectChanges();
310+
});
311+
312+
it('should have no explicit roles', () => {
313+
const tabBar = fixture.nativeElement.querySelector('.mat-tab-nav-bar')!;
314+
expect(tabBar.getAttribute('role')).toBe(null);
315+
316+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-tab-link');
317+
expect(tabLinks[0].getAttribute('role')).toBe(null);
318+
expect(tabLinks[1].getAttribute('role')).toBe(null);
319+
expect(tabLinks[2].getAttribute('role')).toBe(null);
320+
});
321+
322+
it('should not setup aria-controls', () => {
323+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-tab-link');
324+
expect(tabLinks[0].getAttribute('aria-controls')).toBe(null);
325+
expect(tabLinks[1].getAttribute('aria-controls')).toBe(null);
326+
expect(tabLinks[2].getAttribute('aria-controls')).toBe(null);
327+
});
328+
329+
it('should not manage aria-selected', () => {
330+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-tab-link');
331+
expect(tabLinks[0].getAttribute('aria-selected')).toBe(null);
332+
expect(tabLinks[1].getAttribute('aria-selected')).toBe(null);
333+
expect(tabLinks[2].getAttribute('aria-selected')).toBe(null);
334+
});
335+
336+
it('should not activate a link when space is pressed', () => {
337+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-tab-link');
338+
expect(tabLinks[1].classList.contains('mat-tab-label-active')).toBe(false);
339+
340+
dispatchKeyboardEvent(tabLinks[1], 'keydown', SPACE);
341+
fixture.detectChanges();
342+
343+
expect(tabLinks[1].classList.contains('mat-tab-label-active')).toBe(false);
344+
});
345+
});
346+
347+
describe('with panel', () => {
348+
let fixture: ComponentFixture<TabBarWithPanel>;
349+
350+
beforeEach(() => {
351+
fixture = TestBed.createComponent(TabBarWithPanel);
352+
fixture.detectChanges();
353+
});
354+
355+
it('should have the proper roles', () => {
356+
const tabBar = fixture.nativeElement.querySelector('.mat-tab-nav-bar')!;
357+
expect(tabBar.getAttribute('role')).toBe('tablist');
358+
359+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-tab-link');
360+
expect(tabLinks[0].getAttribute('role')).toBe('tab');
361+
expect(tabLinks[1].getAttribute('role')).toBe('tab');
362+
expect(tabLinks[2].getAttribute('role')).toBe('tab');
363+
364+
const tabPanel = fixture.nativeElement.querySelector('.mat-tab-nav-panel')!;
365+
expect(tabPanel.getAttribute('role')).toBe('tabpanel');
366+
});
367+
368+
it('should manage tabindex properly', () => {
369+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-tab-link');
370+
expect(tabLinks[0].tabIndex).toBe(0);
371+
expect(tabLinks[1].tabIndex).toBe(-1);
372+
expect(tabLinks[2].tabIndex).toBe(-1);
373+
374+
tabLinks[1].click();
375+
fixture.detectChanges();
376+
377+
expect(tabLinks[0].tabIndex).toBe(-1);
378+
expect(tabLinks[1].tabIndex).toBe(0);
379+
expect(tabLinks[2].tabIndex).toBe(-1);
380+
});
381+
382+
it('should setup aria-controls properly', () => {
383+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-tab-link');
384+
expect(tabLinks[0].getAttribute('aria-controls')).toBe('tab-panel');
385+
expect(tabLinks[1].getAttribute('aria-controls')).toBe('tab-panel');
386+
expect(tabLinks[2].getAttribute('aria-controls')).toBe('tab-panel');
387+
});
388+
389+
it('should not manage aria-current', () => {
390+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-tab-link');
391+
expect(tabLinks[0].getAttribute('aria-current')).toBe(null);
392+
expect(tabLinks[1].getAttribute('aria-current')).toBe(null);
393+
expect(tabLinks[2].getAttribute('aria-current')).toBe(null);
394+
});
395+
396+
it('should manage aria-selected properly', () => {
397+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-tab-link');
398+
expect(tabLinks[0].getAttribute('aria-selected')).toBe('true');
399+
expect(tabLinks[1].getAttribute('aria-selected')).toBe('false');
400+
expect(tabLinks[2].getAttribute('aria-selected')).toBe('false');
401+
402+
tabLinks[1].click();
403+
fixture.detectChanges();
404+
405+
expect(tabLinks[0].getAttribute('aria-selected')).toBe('false');
406+
expect(tabLinks[1].getAttribute('aria-selected')).toBe('true');
407+
expect(tabLinks[2].getAttribute('aria-selected')).toBe('false');
408+
});
409+
410+
it('should activate a link when space is pressed', () => {
411+
const tabLinks = fixture.nativeElement.querySelectorAll('.mat-tab-link');
412+
expect(tabLinks[1].classList.contains('mat-tab-label-active')).toBe(false);
413+
414+
dispatchKeyboardEvent(tabLinks[1], 'keydown', SPACE);
415+
fixture.detectChanges();
416+
417+
expect(tabLinks[1].classList.contains('mat-tab-label-active')).toBe(true);
418+
});
419+
});
420+
298421
describe('ripples', () => {
299422
let fixture: ComponentFixture<SimpleTabNavBarTestApp>;
300423

@@ -449,3 +572,21 @@ class TabLinkWithNativeTabindexAttr {}
449572
class TabBarWithInactiveTabsOnInit {
450573
tabs = [0, 1, 2];
451574
}
575+
576+
@Component({
577+
template: `
578+
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
579+
<a mat-tab-link
580+
*ngFor="let tab of tabs; let index = index"
581+
[active]="index === activeIndex"
582+
(click)="activeIndex = index">
583+
Tab link
584+
</a>
585+
</nav>
586+
<mat-tab-nav-panel #tabPanel id="tab-panel">Tab panel</mat-tab-nav-panel>
587+
`,
588+
})
589+
class TabBarWithPanel {
590+
tabs = [0, 1, 2];
591+
activeIndex = 0;
592+
}

‎src/material/tabs/tab-nav-bar/tab-nav-bar.ts

+92-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88
import {FocusableOption, FocusMonitor} from '@angular/cdk/a11y';
9+
import {SPACE} from '@angular/cdk/keycodes';
910
import {Directionality} from '@angular/cdk/bidi';
1011
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
1112
import {Platform} from '@angular/cdk/platform';
@@ -50,6 +51,9 @@ import {startWith, takeUntil} from 'rxjs/operators';
5051
import {MatInkBar} from '../ink-bar';
5152
import {MatPaginatedTabHeader, MatPaginatedTabHeaderItem} from '../paginated-tab-header';
5253

54+
// Increasing integer for generating unique ids for tab nav components.
55+
let nextUniqueId = 0;
56+
5357
/**
5458
* Base class with all of the `MatTabNav` functionality.
5559
* @docs-private
@@ -60,7 +64,7 @@ export abstract class _MatTabNavBase
6064
implements AfterContentChecked, AfterContentInit, OnDestroy
6165
{
6266
/** Query list of all tab links of the tab navigation. */
63-
abstract override _items: QueryList<MatPaginatedTabHeaderItem & {active: boolean}>;
67+
abstract override _items: QueryList<MatPaginatedTabHeaderItem & {active: boolean; id: string}>;
6468

6569
/** Background color of the tab nav. */
6670
@Input()
@@ -92,6 +96,13 @@ export abstract class _MatTabNavBase
9296
/** Theme color of the nav bar. */
9397
@Input() color: ThemePalette = 'primary';
9498

99+
/**
100+
* Associated tab panel controlled by the nav bar. If not provided, then the nav bar
101+
* follows the ARIA link / navigation landmark pattern. If provided, it follows the
102+
* ARIA tabs design pattern.
103+
*/
104+
@Input() tabPanel?: MatTabNavPanel;
105+
95106
constructor(
96107
elementRef: ElementRef,
97108
@Optional() dir: Directionality,
@@ -130,6 +141,11 @@ export abstract class _MatTabNavBase
130141
if (items[i].active) {
131142
this.selectedIndex = i;
132143
this._changeDetectorRef.markForCheck();
144+
145+
if (this.tabPanel) {
146+
this.tabPanel._activeTabId = items[i].id;
147+
}
148+
133149
return;
134150
}
135151
}
@@ -138,6 +154,10 @@ export abstract class _MatTabNavBase
138154
this.selectedIndex = -1;
139155
this._inkBar.hide();
140156
}
157+
158+
_getRole(): string | null {
159+
return this.tabPanel ? 'tablist' : this._elementRef.nativeElement.getAttribute('role');
160+
}
141161
}
142162

143163
/**
@@ -151,6 +171,7 @@ export abstract class _MatTabNavBase
151171
templateUrl: 'tab-nav-bar.html',
152172
styleUrls: ['tab-nav-bar.css'],
153173
host: {
174+
'[attr.role]': '_getRole()',
154175
'class': 'mat-tab-nav-bar mat-tab-header',
155176
'[class.mat-tab-header-pagination-controls-enabled]': '_showPaginationControls',
156177
'[class.mat-tab-header-rtl]': "_getLayoutDirection() == 'rtl'",
@@ -238,6 +259,9 @@ export class _MatTabLinkBase
238259
);
239260
}
240261

262+
/** Unique id for the tab. */
263+
@Input() id = `mat-tab-link-${nextUniqueId++}`;
264+
241265
constructor(
242266
private _tabNavBar: _MatTabNavBase,
243267
/** @docs-private */ public elementRef: ElementRef,
@@ -274,6 +298,42 @@ export class _MatTabLinkBase
274298
// have to update the focused index whenever the link receives focus.
275299
this._tabNavBar.focusIndex = this._tabNavBar._items.toArray().indexOf(this);
276300
}
301+
302+
_handleKeydown(event: KeyboardEvent) {
303+
if (this._tabNavBar.tabPanel && event.keyCode === SPACE) {
304+
this.elementRef.nativeElement.click();
305+
}
306+
}
307+
308+
_getAriaControls(): string | null {
309+
return this._tabNavBar.tabPanel
310+
? this._tabNavBar.tabPanel?.id
311+
: this.elementRef.nativeElement.getAttribute('aria-controls');
312+
}
313+
314+
_getAriaSelected(): string | null {
315+
if (this._tabNavBar.tabPanel) {
316+
return this.active ? 'true' : 'false';
317+
} else {
318+
return this.elementRef.nativeElement.getAttribute('aria-selected');
319+
}
320+
}
321+
322+
_getAriaCurrent(): string | null {
323+
return this.active && !this._tabNavBar.tabPanel ? 'page' : null;
324+
}
325+
326+
_getRole(): string | null {
327+
return this._tabNavBar.tabPanel ? 'tab' : this.elementRef.nativeElement.getAttribute('role');
328+
}
329+
330+
_getTabIndex(): number {
331+
if (this._tabNavBar.tabPanel) {
332+
return this._isActive ? 0 : -1;
333+
} else {
334+
return this.tabIndex;
335+
}
336+
}
277337
}
278338

279339
/**
@@ -285,12 +345,17 @@ export class _MatTabLinkBase
285345
inputs: ['disabled', 'disableRipple', 'tabIndex'],
286346
host: {
287347
'class': 'mat-tab-link mat-focus-indicator',
288-
'[attr.aria-current]': 'active ? "page" : null',
348+
'[attr.aria-controls]': '_getAriaControls()',
349+
'[attr.aria-current]': '_getAriaCurrent()',
289350
'[attr.aria-disabled]': 'disabled',
290-
'[attr.tabIndex]': 'tabIndex',
351+
'[attr.aria-selected]': '_getAriaSelected()',
352+
'[attr.id]': 'id',
353+
'[attr.tabIndex]': '_getTabIndex()',
354+
'[attr.role]': '_getRole()',
291355
'[class.mat-tab-disabled]': 'disabled',
292356
'[class.mat-tab-label-active]': 'active',
293357
'(focus)': '_handleFocus()',
358+
'(keydown)': '_handleKeydown($event)',
294359
},
295360
})
296361
export class MatTabLink extends _MatTabLinkBase implements OnDestroy {
@@ -317,3 +382,27 @@ export class MatTabLink extends _MatTabLinkBase implements OnDestroy {
317382
this._tabLinkRipple._removeTriggerEvents();
318383
}
319384
}
385+
386+
/**
387+
* Tab panel component associated with MatTabNav.
388+
*/
389+
@Component({
390+
selector: 'mat-tab-nav-panel',
391+
exportAs: 'matTabNavPanel',
392+
template: '<ng-content></ng-content>',
393+
host: {
394+
'[attr.aria-labelledby]': '_activeTabId',
395+
'[attr.id]': 'id',
396+
'class': 'mat-tab-nav-panel',
397+
'role': 'tabpanel',
398+
},
399+
encapsulation: ViewEncapsulation.None,
400+
changeDetection: ChangeDetectionStrategy.OnPush,
401+
})
402+
export class MatTabNavPanel {
403+
/** Unique id for the tab panel. */
404+
@Input() id = `mat-tab-nav-panel-${nextUniqueId++}`;
405+
406+
/** Id of the active tab in the nav bar. */
407+
_activeTabId?: string;
408+
}

‎src/material/tabs/tabs-module.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {MatTabGroup} from './tab-group';
2020
import {MatTabHeader} from './tab-header';
2121
import {MatTabLabel} from './tab-label';
2222
import {MatTabLabelWrapper} from './tab-label-wrapper';
23-
import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
23+
import {MatTabLink, MatTabNav, MatTabNavPanel} from './tab-nav-bar/tab-nav-bar';
2424

2525
@NgModule({
2626
imports: [
@@ -38,6 +38,7 @@ import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
3838
MatTabLabel,
3939
MatTab,
4040
MatTabNav,
41+
MatTabNavPanel,
4142
MatTabLink,
4243
MatTabContent,
4344
],
@@ -48,6 +49,7 @@ import {MatTabLink, MatTabNav} from './tab-nav-bar/tab-nav-bar';
4849
MatInkBar,
4950
MatTabLabelWrapper,
5051
MatTabNav,
52+
MatTabNavPanel,
5153
MatTabLink,
5254
MatTabBody,
5355
MatTabBodyPortal,

‎src/material/tabs/testing/tab-harness-filters.ts

+3
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ export interface TabLinkHarnessFilters extends BaseHarnessFilters {
2727

2828
/** A set of criteria that can be used to filter a list of `MatTabNavBarHarness` instances. */
2929
export interface TabNavBarHarnessFilters extends BaseHarnessFilters {}
30+
31+
/** A set of criteria that can be used to filter a list of `MatTabNavBarHarness` instances. */
32+
export interface TabNavPanelHarnessFilters extends BaseHarnessFilters {}

‎src/material/tabs/testing/tab-nav-bar-harness.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
*/
88

99
import {ComponentHarness, HarnessPredicate, parallel} from '@angular/cdk/testing';
10-
import {TabNavBarHarnessFilters, TabLinkHarnessFilters} from './tab-harness-filters';
10+
import {
11+
TabNavBarHarnessFilters,
12+
TabNavPanelHarnessFilters,
13+
TabLinkHarnessFilters,
14+
} from './tab-harness-filters';
1115
import {MatTabLinkHarness} from './tab-link-harness';
16+
import {MatTabNavPanelHarness} from './tab-nav-panel-harness';
1217

1318
/** Harness for interacting with a standard mat-tab-nav-bar in tests. */
1419
export class MatTabNavBarHarness extends ComponentHarness {
@@ -57,4 +62,17 @@ export class MatTabNavBarHarness extends ComponentHarness {
5762
}
5863
await tabs[0].click();
5964
}
65+
66+
/** Gets the panel associated with the nav bar. */
67+
async getPanel(): Promise<MatTabNavPanelHarness> {
68+
const link = await this.getActiveLink();
69+
const host = await link.host();
70+
const panelId = await host.getAttribute('aria-controls');
71+
if (!panelId) {
72+
throw Error('No panel is controlled by the nav bar.');
73+
}
74+
75+
const filter: TabNavPanelHarnessFilters = {selector: `#${panelId}`};
76+
return await this.documentRootLocatorFactory().locatorFor(MatTabNavPanelHarness.with(filter))();
77+
}
6078
}

‎src/material/tabs/testing/tab-nav-bar-shared.spec.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -92,11 +92,17 @@ export function runTabNavBarHarnessTests(
9292
expect(await links[1].isActive()).toBe(true);
9393
expect(await links[2].isActive()).toBe(false);
9494
});
95+
96+
it('should be able to get the associated tab panel', async () => {
97+
const navBar = await loader.getHarness(tabNavBarHarness);
98+
const navPanel = await navBar.getPanel();
99+
expect(await navPanel.getTextContent()).toBe('Tab content');
100+
});
95101
}
96102

97103
@Component({
98104
template: `
99-
<nav mat-tab-nav-bar>
105+
<nav mat-tab-nav-bar [tabPanel]="tabPanel">
100106
<a href="#" (click)="select(0, $event)" [active]="activeLink === 0" matTabLink>First</a>
101107
<a href="#" (click)="select(1, $event)" [active]="activeLink === 1" matTabLink>Second</a>
102108
<a
@@ -106,6 +112,9 @@ export function runTabNavBarHarnessTests(
106112
[disabled]="isDisabled"
107113
matTabLink>Third</a>
108114
</nav>
115+
<mat-tab-nav-panel #tabPanel id="tab-panel">
116+
Tab content
117+
</mat-tab-nav-panel>
109118
`,
110119
})
111120
class TabNavBarHarnessTest {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {ContentContainerComponentHarness, HarnessPredicate} from '@angular/cdk/testing';
10+
import {TabNavPanelHarnessFilters} from './tab-harness-filters';
11+
12+
/** Harness for interacting with a standard mat-tab-nav-panel in tests. */
13+
export class MatTabNavPanelHarness extends ContentContainerComponentHarness {
14+
/** The selector for the host element of a `MatTabNavPanel` instance. */
15+
static hostSelector = '.mat-tab-nav-panel';
16+
17+
/**
18+
* Gets a `HarnessPredicate` that can be used to search for a `MatTabNavPanel` that meets
19+
* certain criteria.
20+
* @param options Options for filtering which tab nav panel instances are considered a match.
21+
* @return a `HarnessPredicate` configured with the given options.
22+
*/
23+
static with(options: TabNavPanelHarnessFilters = {}): HarnessPredicate<MatTabNavPanelHarness> {
24+
return new HarnessPredicate(MatTabNavPanelHarness, options);
25+
}
26+
27+
/** Gets the tab panel text content. */
28+
async getTextContent(): Promise<string> {
29+
return (await this.host()).text();
30+
}
31+
}

‎tools/public_api_guard/material/tabs-testing.md

+5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export class MatTabNavBarHarness extends ComponentHarness {
4949
clickLink(filter?: TabLinkHarnessFilters): Promise<void>;
5050
getActiveLink(): Promise<MatTabLinkHarness>;
5151
getLinks(filter?: TabLinkHarnessFilters): Promise<MatTabLinkHarness[]>;
52+
getPanel(): Promise<MatTabNavPanelHarness>;
5253
static hostSelector: string;
5354
static with(options?: TabNavBarHarnessFilters): HarnessPredicate<MatTabNavBarHarness>;
5455
}
@@ -72,6 +73,10 @@ export interface TabLinkHarnessFilters extends BaseHarnessFilters {
7273
export interface TabNavBarHarnessFilters extends BaseHarnessFilters {
7374
}
7475

76+
// @public
77+
export interface TabNavPanelHarnessFilters extends BaseHarnessFilters {
78+
}
79+
7580
// (No @packageDocumentation comment for this package)
7681

7782
```

‎tools/public_api_guard/material/tabs.md

+30-3
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,20 @@ export class _MatTabLinkBase extends _MatTabLinkMixinBase implements AfterViewIn
352352
elementRef: ElementRef;
353353
focus(): void;
354354
// (undocumented)
355+
_getAriaControls(): string | null;
356+
// (undocumented)
357+
_getAriaCurrent(): string | null;
358+
// (undocumented)
359+
_getAriaSelected(): string | null;
360+
// (undocumented)
361+
_getRole(): string | null;
362+
// (undocumented)
363+
_getTabIndex(): number;
364+
// (undocumented)
355365
_handleFocus(): void;
366+
// (undocumented)
367+
_handleKeydown(event: KeyboardEvent): void;
368+
id: string;
356369
protected _isActive: boolean;
357370
// (undocumented)
358371
ngAfterViewInit(): void;
@@ -361,7 +374,7 @@ export class _MatTabLinkBase extends _MatTabLinkMixinBase implements AfterViewIn
361374
rippleConfig: RippleConfig & RippleGlobalOptions;
362375
get rippleDisabled(): boolean;
363376
// (undocumented)
364-
static ɵdir: i0.ɵɵDirectiveDeclaration<_MatTabLinkBase, never, never, { "active": "active"; }, {}, never>;
377+
static ɵdir: i0.ɵɵDirectiveDeclaration<_MatTabLinkBase, never, never, { "active": "active"; "id": "id"; }, {}, never>;
365378
// (undocumented)
366379
static ɵfac: i0.ɵɵFactoryDeclaration<_MatTabLinkBase, [null, null, { optional: true; }, { attribute: "tabindex"; }, null, { optional: true; }]>;
367380
}
@@ -397,20 +410,34 @@ export abstract class _MatTabNavBase extends MatPaginatedTabHeader implements Af
397410
color: ThemePalette;
398411
get disableRipple(): boolean;
399412
set disableRipple(value: BooleanInput);
413+
// (undocumented)
414+
_getRole(): string | null;
400415
abstract _items: QueryList<MatPaginatedTabHeaderItem & {
401416
active: boolean;
417+
id: string;
402418
}>;
403419
// (undocumented)
404420
protected _itemSelected(): void;
405421
// (undocumented)
406422
ngAfterContentInit(): void;
423+
tabPanel?: MatTabNavPanel;
407424
updateActiveLink(): void;
408425
// (undocumented)
409-
static ɵdir: i0.ɵɵDirectiveDeclaration<_MatTabNavBase, never, never, { "backgroundColor": "backgroundColor"; "disableRipple": "disableRipple"; "color": "color"; }, {}, never>;
426+
static ɵdir: i0.ɵɵDirectiveDeclaration<_MatTabNavBase, never, never, { "backgroundColor": "backgroundColor"; "disableRipple": "disableRipple"; "color": "color"; "tabPanel": "tabPanel"; }, {}, never>;
410427
// (undocumented)
411428
static ɵfac: i0.ɵɵFactoryDeclaration<_MatTabNavBase, [null, { optional: true; }, null, null, null, null, { optional: true; }]>;
412429
}
413430

431+
// @public
432+
export class MatTabNavPanel {
433+
_activeTabId?: string;
434+
id: string;
435+
// (undocumented)
436+
static ɵcmp: i0.ɵɵComponentDeclaration<MatTabNavPanel, "mat-tab-nav-panel", ["matTabNavPanel"], { "id": "id"; }, {}, never, ["*"]>;
437+
// (undocumented)
438+
static ɵfac: i0.ɵɵFactoryDeclaration<MatTabNavPanel, never>;
439+
}
440+
414441
// @public
415442
export const matTabsAnimations: {
416443
readonly translateTab: AnimationTriggerMetadata;
@@ -432,7 +459,7 @@ export class MatTabsModule {
432459
// (undocumented)
433460
static ɵinj: i0.ɵɵInjectorDeclaration<MatTabsModule>;
434461
// (undocumented)
435-
static ɵmod: i0.ɵɵNgModuleDeclaration<MatTabsModule, [typeof i1.MatTabGroup, typeof i2.MatTabLabel, typeof i3.MatTab, typeof i4.MatInkBar, typeof i5.MatTabLabelWrapper, typeof i6.MatTabNav, typeof i6.MatTabLink, typeof i7.MatTabBody, typeof i7.MatTabBodyPortal, typeof i8.MatTabHeader, typeof i9.MatTabContent], [typeof i10.CommonModule, typeof i11.MatCommonModule, typeof i12.PortalModule, typeof i11.MatRippleModule, typeof i13.ObserversModule, typeof i14.A11yModule], [typeof i11.MatCommonModule, typeof i1.MatTabGroup, typeof i2.MatTabLabel, typeof i3.MatTab, typeof i6.MatTabNav, typeof i6.MatTabLink, typeof i9.MatTabContent]>;
462+
static ɵmod: i0.ɵɵNgModuleDeclaration<MatTabsModule, [typeof i1.MatTabGroup, typeof i2.MatTabLabel, typeof i3.MatTab, typeof i4.MatInkBar, typeof i5.MatTabLabelWrapper, typeof i6.MatTabNav, typeof i6.MatTabNavPanel, typeof i6.MatTabLink, typeof i7.MatTabBody, typeof i7.MatTabBodyPortal, typeof i8.MatTabHeader, typeof i9.MatTabContent], [typeof i10.CommonModule, typeof i11.MatCommonModule, typeof i12.PortalModule, typeof i11.MatRippleModule, typeof i13.ObserversModule, typeof i14.A11yModule], [typeof i11.MatCommonModule, typeof i1.MatTabGroup, typeof i2.MatTabLabel, typeof i3.MatTab, typeof i6.MatTabNav, typeof i6.MatTabNavPanel, typeof i6.MatTabLink, typeof i9.MatTabContent]>;
436463
}
437464

438465
// @public

0 commit comments

Comments
 (0)
Please sign in to comment.