Skip to content

Commit 838d1a4

Browse files
committedAug 14, 2024·
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 b2e728d commit 838d1a4

File tree

6 files changed

+62
-2
lines changed

6 files changed

+62
-2
lines changed
 

‎src/material/tabs/tab-group.html

+2
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

+40
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,42 @@ describe('MatTabGroup', () => {
409409

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

414450
describe('aria labelling', () => {
@@ -1159,6 +1195,8 @@ describe('MatTabNavBar with a default config', () => {
11591195
[headerPosition]="headerPosition"
11601196
[disableRipple]="disableRipple"
11611197
[contentTabIndex]="contentTabIndex"
1198+
[aria-label]="ariaLabel"
1199+
[aria-labelledby]="ariaLabelledby"
11621200
(animationDone)="animationDone()"
11631201
(focusChange)="handleFocus($event)"
11641202
(selectedTabChange)="handleSelection($event)">
@@ -1188,6 +1226,8 @@ class SimpleTabsTestApp {
11881226
disableRipple: boolean = false;
11891227
contentTabIndex: number | null = null;
11901228
headerPosition: MatTabHeaderPosition = 'above';
1229+
ariaLabel: string;
1230+
ariaLabelledby: string;
11911231
handleFocus(event: any) {
11921232
this.focusEvent = event;
11931233
}

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

+6
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

+2
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

+6
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

+6-2
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)
Please sign in to comment.