Skip to content

Commit 6f369d5

Browse files
committed
feat(material/tabs): Refactor MatTabNav to follow the ARIA tab design pattern.
1 parent 8ae41b0 commit 6f369d5

File tree

8 files changed

+123
-7
lines changed

8 files changed

+123
-7
lines changed

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

Lines changed: 3 additions & 0 deletions
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 {TabNavBarWithOutletExample} from './tab-nav-bar-with-outlet/tab-nav-bar-with-outlet-example';
2324

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

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

5659
@NgModule({
Lines changed: 4 additions & 0 deletions
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+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!-- #docregion mat-tab-nav -->
2+
<nav mat-tab-nav-bar [backgroundColor]="background" [outlet]="outlet">
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-outlet #outlet></mat-tab-nav-outlet>
9+
<!-- #enddocregion mat-tab-nav -->
10+
11+
<button mat-raised-button class="example-action-button" (click)="toggleBackground()">
12+
Toggle background
13+
</button>
14+
<button mat-raised-button class="example-action-button" (click)="addLink()">
15+
Add link
16+
</button>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {Component} from '@angular/core';
2+
import {ThemePalette} from '@angular/material/core';
3+
4+
/**
5+
* @title Use of the tab nav bar with the dedicated outlet component.
6+
*/
7+
@Component({
8+
selector: 'tab-nav-bar-with-outlet-example',
9+
templateUrl: 'tab-nav-bar-with-outlet-example.html',
10+
styleUrls: ['tab-nav-bar-with-outlet-example.css'],
11+
})
12+
export class TabNavBarWithOutletExample {
13+
links = ['First', 'Second', 'Third'];
14+
activeLink = this.links[0];
15+
background: ThemePalette = undefined;
16+
17+
toggleBackground() {
18+
this.background = this.background ? undefined : 'primary';
19+
}
20+
21+
addLink() {
22+
this.links.push(`Link ${this.links.length + 1}`);
23+
}
24+
}

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

Lines changed: 3 additions & 1 deletion
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 outlet</h3>
24+
<tab-nav-bar-with-outlet-example></tab-nav-bar-with-outlet-example>

src/material/tabs/public-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ 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 {MatTabNav, MatTabLink, MatTabNavOutlet, _MatTabNavBase, _MatTabLinkBase} from './tab-nav-bar/index';
2424
export {MatTabContent} from './tab-content';
2525
export {ScrollDirection} from './paginated-tab-header';
2626
export * from './tabs-animations';

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

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
Input,
2727
NgZone,
2828
OnDestroy,
29+
OnInit,
2930
Optional,
3031
QueryList,
3132
ViewChild,
@@ -60,7 +61,7 @@ export abstract class _MatTabNavBase
6061
implements AfterContentChecked, AfterContentInit, OnDestroy
6162
{
6263
/** Query list of all tab links of the tab navigation. */
63-
abstract override _items: QueryList<MatPaginatedTabHeaderItem & {active: boolean}>;
64+
abstract override _items: QueryList<MatPaginatedTabHeaderItem & {active: boolean, _uniqueId: string}>;
6465

6566
/** Background color of the tab nav. */
6667
@Input()
@@ -92,6 +93,14 @@ export abstract class _MatTabNavBase
9293
/** Theme color of the nav bar. */
9394
@Input() color: ThemePalette = 'primary';
9495

96+
/** Associated outlet controlled by the nav bar. */
97+
@Input() outlet?: MatTabNavOutlet;
98+
99+
/** Returns whether the nav bar should follow the ARIA tab design pattern. */
100+
get _isTabDesignPattern() {
101+
return !!this.outlet;
102+
}
103+
95104
constructor(
96105
elementRef: ElementRef,
97106
@Optional() dir: Directionality,
@@ -130,6 +139,12 @@ export abstract class _MatTabNavBase
130139
if (items[i].active) {
131140
this.selectedIndex = i;
132141
this._changeDetectorRef.markForCheck();
142+
143+
if (this._isTabDesignPattern) {
144+
this.outlet!._activeTabId = items[i]._uniqueId;
145+
this.outlet!._cdr.markForCheck();
146+
}
147+
133148
return;
134149
}
135150
}
@@ -157,6 +172,7 @@ export abstract class _MatTabNavBase
157172
'[class.mat-primary]': 'color !== "warn" && color !== "accent"',
158173
'[class.mat-accent]': 'color === "accent"',
159174
'[class.mat-warn]': 'color === "warn"',
175+
'[attr.role]': '_isTabDesignPattern ? "tablist" : null'
160176
},
161177
encapsulation: ViewEncapsulation.None,
162178
// tslint:disable-next-line:validate-decorators
@@ -186,6 +202,9 @@ export class MatTabNav extends _MatTabNavBase {
186202
static ngAcceptInputType_disableRipple: BooleanInput;
187203
}
188204

205+
// Increasing integer for generating unique ids for tab link components.
206+
let nextUniqueTabLinkId = 0;
207+
189208
// Boilerplate for applying mixins to MatTabLink.
190209
const _MatTabLinkMixinBase = mixinTabIndex(mixinDisableRipple(mixinDisabled(class {})));
191210

@@ -240,6 +259,13 @@ export class _MatTabLinkBase
240259
);
241260
}
242261

262+
get _index(): number {
263+
return this._tabNavBar._items.toArray().indexOf(this);
264+
}
265+
266+
/** Unique id for the component referenced in ARIA attributes. */
267+
_uniqueId = `mat-tab-link-${nextUniqueTabLinkId++}`;
268+
243269
constructor(
244270
private _tabNavBar: _MatTabNavBase,
245271
/** @docs-private */ public elementRef: ElementRef,
@@ -274,7 +300,21 @@ export class _MatTabLinkBase
274300
_handleFocus() {
275301
// Since we allow navigation through tabbing in the nav bar, we
276302
// have to update the focused index whenever the link receives focus.
277-
this._tabNavBar.focusIndex = this._tabNavBar._items.toArray().indexOf(this);
303+
this._tabNavBar.focusIndex = this._index;
304+
}
305+
306+
_handleSpace() {
307+
if (this._tabNavBar._isTabDesignPattern) {
308+
this.elementRef.nativeElement.click();
309+
}
310+
}
311+
312+
_getTabIndex() {
313+
if (!this._tabNavBar._items) {
314+
return this._isActive ? '0' : '-1';
315+
}
316+
317+
return (this._tabNavBar.focusIndex === this._index) ? '0' : '-1';
278318
}
279319

280320
static ngAcceptInputType_active: BooleanInput;
@@ -292,12 +332,16 @@ export class _MatTabLinkBase
292332
inputs: ['disabled', 'disableRipple', 'tabIndex'],
293333
host: {
294334
'class': 'mat-tab-link mat-focus-indicator',
295-
'[attr.aria-current]': 'active ? "page" : null',
335+
'[attr.aria-controls]': '_tabNavBar._isTabDesignPattern ? _tabNavBar.outlet._uniqueId : null',
336+
'[attr.aria-current]': 'active && !_tabNavBar._isTabDesignPattern ? "page" : null',
296337
'[attr.aria-disabled]': 'disabled',
297-
'[attr.tabIndex]': 'tabIndex',
338+
'[attr.aria-selected]': '_tabNavBar._isTabDesignPattern ? (active ? "true" : "false") : null',
339+
'[attr.tabIndex]': '_tabNavBar._isTabDesignPattern ? _getTabIndex() : tabIndex',
340+
'[attr.role]': '_tabNavBar._isTabDesignPattern ? "tab" : null',
298341
'[class.mat-tab-disabled]': 'disabled',
299342
'[class.mat-tab-label-active]': 'active',
300343
'(focus)': '_handleFocus()',
344+
'(keydown.space)': '_handleSpace()'
301345
},
302346
})
303347
export class MatTabLink extends _MatTabLinkBase implements OnDestroy {
@@ -324,3 +368,24 @@ export class MatTabLink extends _MatTabLinkBase implements OnDestroy {
324368
this._tabLinkRipple._removeTriggerEvents();
325369
}
326370
}
371+
372+
// Increasing integer for generating unique ids for tab nav outlet components.
373+
let nextUniqueTabNavOutletId = 0;
374+
375+
@Component({
376+
selector: 'mat-tab-nav-outlet',
377+
exportAs: 'matTabNavOutlet',
378+
template: '<ng-content select="router-outlet"></ng-content>',
379+
host: {
380+
'[attr.aria-labelledby]': '_activeTabId || null',
381+
'role': 'tabpanel',
382+
},
383+
})
384+
export class MatTabNavOutlet {
385+
_activeTabId?: string;
386+
387+
/** Unique id for the component referenced in ARIA attributes. */
388+
_uniqueId = `mat-tab-nav-outlet-${nextUniqueTabNavOutletId++}`;
389+
390+
constructor(readonly _cdr: ChangeDetectorRef) {}
391+
}

src/material/tabs/tabs-module.ts

Lines changed: 3 additions & 1 deletion
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, MatTabNavOutlet} 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+
MatTabNavOutlet,
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+
MatTabNavOutlet,
5153
MatTabLink,
5254
MatTabBody,
5355
MatTabBodyPortal,

0 commit comments

Comments
 (0)