Skip to content

Commit f7a0305

Browse files
committed
fix(material/tabs): allow for tablist aria-label and aria-labelledby to be set (#29562)
According to the [W3C reference implementation](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/), the inner `tablist` can be labelled using `aria-label` or `aria-labelledby`. These changes add an input to allow them to be set. Fixes #29486. (cherry picked from commit 1968cc4)
1 parent 1ee9d49 commit f7a0305

File tree

6 files changed

+62
-2
lines changed

6 files changed

+62
-2
lines changed

src/material/tabs/tab-group.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
[selectedIndex]="selectedIndex || 0"
33
[disableRipple]="disableRipple"
44
[disablePagination]="disablePagination"
5+
[aria-label]="ariaLabel"
6+
[aria-labelledby]="ariaLabelledby"
57
(indexFocused)="_focusChanged($event)"
68
(selectFocusedIndex)="selectedIndex = $event">
79

src/material/tabs/tab-group.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,42 @@ describe('MatTabGroup', () => {
407407

408408
expect(tabLabels.map(label => label.getAttribute('tabindex'))).toEqual(['-1', '-1', '0']);
409409
});
410+
411+
it('should be able to set the aria-label of the tablist', fakeAsync(() => {
412+
fixture.detectChanges();
413+
tick();
414+
415+
const tabList = fixture.nativeElement.querySelector('.mat-mdc-tab-list') as HTMLElement;
416+
expect(tabList.hasAttribute('aria-label')).toBe(false);
417+
418+
fixture.componentInstance.ariaLabel = 'hello';
419+
fixture.changeDetectorRef.markForCheck();
420+
fixture.detectChanges();
421+
expect(tabList.getAttribute('aria-label')).toBe('hello');
422+
423+
fixture.componentInstance.ariaLabel = '';
424+
fixture.changeDetectorRef.markForCheck();
425+
fixture.detectChanges();
426+
expect(tabList.hasAttribute('aria-label')).toBe(false);
427+
}));
428+
429+
it('should be able to set the aria-labelledby of the tablist', fakeAsync(() => {
430+
fixture.detectChanges();
431+
tick();
432+
433+
const tabList = fixture.nativeElement.querySelector('.mat-mdc-tab-list') as HTMLElement;
434+
expect(tabList.hasAttribute('aria-labelledby')).toBe(false);
435+
436+
fixture.componentInstance.ariaLabelledby = 'some-label';
437+
fixture.changeDetectorRef.markForCheck();
438+
fixture.detectChanges();
439+
expect(tabList.getAttribute('aria-labelledby')).toBe('some-label');
440+
441+
fixture.componentInstance.ariaLabelledby = '';
442+
fixture.changeDetectorRef.markForCheck();
443+
fixture.detectChanges();
444+
expect(tabList.hasAttribute('aria-labelledby')).toBe(false);
445+
}));
410446
});
411447

412448
describe('aria labelling', () => {
@@ -1151,6 +1187,8 @@ describe('MatTabNavBar with a default config', () => {
11511187
[headerPosition]="headerPosition"
11521188
[disableRipple]="disableRipple"
11531189
[contentTabIndex]="contentTabIndex"
1190+
[aria-label]="ariaLabel"
1191+
[aria-labelledby]="ariaLabelledby"
11541192
(animationDone)="animationDone()"
11551193
(focusChange)="handleFocus($event)"
11561194
(selectedTabChange)="handleSelection($event)">
@@ -1180,6 +1218,8 @@ class SimpleTabsTestApp {
11801218
disableRipple: boolean = false;
11811219
contentTabIndex: number | null = null;
11821220
headerPosition: MatTabHeaderPosition = 'above';
1221+
ariaLabel: string;
1222+
ariaLabelledby: string;
11831223
handleFocus(event: any) {
11841224
this.focusEvent = event;
11851225
}

src/material/tabs/tab-group.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,12 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes
242242

243243
private _backgroundColor: ThemePalette;
244244

245+
/** Aria label of the inner `tablist` of the group. */
246+
@Input('aria-label') ariaLabel: string;
247+
248+
/** Sets the `aria-labelledby` of the inner `tablist` of the group. */
249+
@Input('aria-labelledby') ariaLabelledby: string;
250+
245251
/** Output to enable support for two-way binding on `[(selectedIndex)]` */
246252
@Output() readonly selectedIndexChange: EventEmitter<number> = new EventEmitter<number>();
247253

src/material/tabs/tab-header.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
#tabList
2323
class="mat-mdc-tab-list"
2424
role="tablist"
25+
[attr.aria-label]="ariaLabel || null"
26+
[attr.aria-labelledby]="ariaLabelledby || null"
2527
(cdkObserveContent)="_onContentChanges()">
2628
<div class="mat-mdc-tab-labels" #tabListInner>
2729
<ng-content></ng-content>

src/material/tabs/tab-header.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ export class MatTabHeader
6969
@ViewChild('previousPaginator') _previousPaginator: ElementRef<HTMLElement>;
7070
_inkBar: MatInkBar;
7171

72+
/** Aria label of the header. */
73+
@Input('aria-label') ariaLabel: string;
74+
75+
/** Sets the `aria-labelledby` of the header. */
76+
@Input('aria-labelledby') ariaLabelledby: string;
77+
7278
/** Whether the ripple effect is disabled or not. */
7379
@Input({transform: booleanAttribute})
7480
disableRipple: boolean = false;

tools/public_api_guard/material/tabs.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes
259259
set animationDuration(value: string | number);
260260
// (undocumented)
261261
_animationMode?: string | undefined;
262+
ariaLabel: string;
263+
ariaLabelledby: string;
262264
// @deprecated
263265
get backgroundColor(): ThemePalette;
264266
set backgroundColor(value: ThemePalette);
@@ -320,7 +322,7 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes
320322
_tabs: QueryList<MatTab>;
321323
updatePagination(): void;
322324
// (undocumented)
323-
static ɵcmp: i0.ɵɵComponentDeclaration<MatTabGroup, "mat-tab-group", ["matTabGroup"], { "color": { "alias": "color"; "required": false; }; "fitInkBarToContent": { "alias": "fitInkBarToContent"; "required": false; }; "stretchTabs": { "alias": "mat-stretch-tabs"; "required": false; }; "dynamicHeight": { "alias": "dynamicHeight"; "required": false; }; "selectedIndex": { "alias": "selectedIndex"; "required": false; }; "headerPosition": { "alias": "headerPosition"; "required": false; }; "animationDuration": { "alias": "animationDuration"; "required": false; }; "contentTabIndex": { "alias": "contentTabIndex"; "required": false; }; "disablePagination": { "alias": "disablePagination"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "preserveContent": { "alias": "preserveContent"; "required": false; }; "backgroundColor": { "alias": "backgroundColor"; "required": false; }; }, { "selectedIndexChange": "selectedIndexChange"; "focusChange": "focusChange"; "animationDone": "animationDone"; "selectedTabChange": "selectedTabChange"; }, ["_allTabs"], ["*"], true, never>;
325+
static ɵcmp: i0.ɵɵComponentDeclaration<MatTabGroup, "mat-tab-group", ["matTabGroup"], { "color": { "alias": "color"; "required": false; }; "fitInkBarToContent": { "alias": "fitInkBarToContent"; "required": false; }; "stretchTabs": { "alias": "mat-stretch-tabs"; "required": false; }; "dynamicHeight": { "alias": "dynamicHeight"; "required": false; }; "selectedIndex": { "alias": "selectedIndex"; "required": false; }; "headerPosition": { "alias": "headerPosition"; "required": false; }; "animationDuration": { "alias": "animationDuration"; "required": false; }; "contentTabIndex": { "alias": "contentTabIndex"; "required": false; }; "disablePagination": { "alias": "disablePagination"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "preserveContent": { "alias": "preserveContent"; "required": false; }; "backgroundColor": { "alias": "backgroundColor"; "required": false; }; "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; }; }, { "selectedIndexChange": "selectedIndexChange"; "focusChange": "focusChange"; "animationDone": "animationDone"; "selectedTabChange": "selectedTabChange"; }, ["_allTabs"], ["*"], true, never>;
324326
// (undocumented)
325327
static ɵfac: i0.ɵɵFactoryDeclaration<MatTabGroup, [null, null, { optional: true; }, { optional: true; }]>;
326328
}
@@ -338,6 +340,8 @@ export interface MatTabGroupBaseHeader {
338340
// @public
339341
export class MatTabHeader extends MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy {
340342
constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, viewportRuler: ViewportRuler, dir: Directionality, ngZone: NgZone, platform: Platform, animationMode?: string);
343+
ariaLabel: string;
344+
ariaLabelledby: string;
341345
disableRipple: boolean;
342346
// (undocumented)
343347
_inkBar: MatInkBar;
@@ -360,7 +364,7 @@ export class MatTabHeader extends MatPaginatedTabHeader implements AfterContentC
360364
// (undocumented)
361365
_tabListInner: ElementRef;
362366
// (undocumented)
363-
static ɵcmp: i0.ɵɵComponentDeclaration<MatTabHeader, "mat-tab-header", never, { "disableRipple": { "alias": "disableRipple"; "required": false; }; }, {}, ["_items"], ["*"], true, never>;
367+
static ɵcmp: i0.ɵɵComponentDeclaration<MatTabHeader, "mat-tab-header", never, { "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; }, {}, ["_items"], ["*"], true, never>;
364368
// (undocumented)
365369
static ɵfac: i0.ɵɵFactoryDeclaration<MatTabHeader, [null, null, null, { optional: true; }, null, null, { optional: true; }]>;
366370
}

0 commit comments

Comments
 (0)