@@ -26,6 +26,7 @@ import {
26
26
Input ,
27
27
NgZone ,
28
28
OnDestroy ,
29
+ OnInit ,
29
30
Optional ,
30
31
QueryList ,
31
32
ViewChild ,
@@ -60,7 +61,7 @@ export abstract class _MatTabNavBase
60
61
implements AfterContentChecked , AfterContentInit , OnDestroy
61
62
{
62
63
/** 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 } > ;
64
65
65
66
/** Background color of the tab nav. */
66
67
@Input ( )
@@ -92,6 +93,14 @@ export abstract class _MatTabNavBase
92
93
/** Theme color of the nav bar. */
93
94
@Input ( ) color : ThemePalette = 'primary' ;
94
95
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
+
95
104
constructor (
96
105
elementRef : ElementRef ,
97
106
@Optional ( ) dir : Directionality ,
@@ -130,6 +139,12 @@ export abstract class _MatTabNavBase
130
139
if ( items [ i ] . active ) {
131
140
this . selectedIndex = i ;
132
141
this . _changeDetectorRef . markForCheck ( ) ;
142
+
143
+ if ( this . _isTabDesignPattern ) {
144
+ this . outlet ! . _activeTabId = items [ i ] . _uniqueId ;
145
+ this . outlet ! . _cdr . markForCheck ( ) ;
146
+ }
147
+
133
148
return ;
134
149
}
135
150
}
@@ -157,6 +172,7 @@ export abstract class _MatTabNavBase
157
172
'[class.mat-primary]' : 'color !== "warn" && color !== "accent"' ,
158
173
'[class.mat-accent]' : 'color === "accent"' ,
159
174
'[class.mat-warn]' : 'color === "warn"' ,
175
+ '[attr.role]' : '_isTabDesignPattern ? "tablist" : null'
160
176
} ,
161
177
encapsulation : ViewEncapsulation . None ,
162
178
// tslint:disable-next-line:validate-decorators
@@ -186,6 +202,9 @@ export class MatTabNav extends _MatTabNavBase {
186
202
static ngAcceptInputType_disableRipple : BooleanInput ;
187
203
}
188
204
205
+ // Increasing integer for generating unique ids for tab link components.
206
+ let nextUniqueTabLinkId = 0 ;
207
+
189
208
// Boilerplate for applying mixins to MatTabLink.
190
209
const _MatTabLinkMixinBase = mixinTabIndex ( mixinDisableRipple ( mixinDisabled ( class { } ) ) ) ;
191
210
@@ -240,6 +259,13 @@ export class _MatTabLinkBase
240
259
) ;
241
260
}
242
261
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
+
243
269
constructor (
244
270
private _tabNavBar : _MatTabNavBase ,
245
271
/** @docs -private */ public elementRef : ElementRef ,
@@ -274,7 +300,21 @@ export class _MatTabLinkBase
274
300
_handleFocus ( ) {
275
301
// Since we allow navigation through tabbing in the nav bar, we
276
302
// 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' ;
278
318
}
279
319
280
320
static ngAcceptInputType_active : BooleanInput ;
@@ -292,12 +332,16 @@ export class _MatTabLinkBase
292
332
inputs : [ 'disabled' , 'disableRipple' , 'tabIndex' ] ,
293
333
host : {
294
334
'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' ,
296
337
'[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' ,
298
341
'[class.mat-tab-disabled]' : 'disabled' ,
299
342
'[class.mat-tab-label-active]' : 'active' ,
300
343
'(focus)' : '_handleFocus()' ,
344
+ '(keydown.space)' : '_handleSpace()'
301
345
} ,
302
346
} )
303
347
export class MatTabLink extends _MatTabLinkBase implements OnDestroy {
@@ -324,3 +368,24 @@ export class MatTabLink extends _MatTabLinkBase implements OnDestroy {
324
368
this . _tabLinkRipple . _removeTriggerEvents ( ) ;
325
369
}
326
370
}
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
+ }
0 commit comments