Skip to content

Commit

Permalink
fix(material/tabs): allow for tablist aria-label and aria-labelledby …
Browse files Browse the repository at this point in the history
…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)
  • Loading branch information
crisbeto committed Aug 14, 2024
1 parent 1ee9d49 commit f7a0305
Show file tree
Hide file tree
Showing 6 changed files with 62 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/material/tabs/tab-group.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
[selectedIndex]="selectedIndex || 0"
[disableRipple]="disableRipple"
[disablePagination]="disablePagination"
[aria-label]="ariaLabel"
[aria-labelledby]="ariaLabelledby"
(indexFocused)="_focusChanged($event)"
(selectFocusedIndex)="selectedIndex = $event">

Expand Down
40 changes: 40 additions & 0 deletions src/material/tabs/tab-group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,42 @@ describe('MatTabGroup', () => {

expect(tabLabels.map(label => label.getAttribute('tabindex'))).toEqual(['-1', '-1', '0']);
});

it('should be able to set the aria-label of the tablist', fakeAsync(() => {
fixture.detectChanges();
tick();

const tabList = fixture.nativeElement.querySelector('.mat-mdc-tab-list') as HTMLElement;
expect(tabList.hasAttribute('aria-label')).toBe(false);

fixture.componentInstance.ariaLabel = 'hello';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(tabList.getAttribute('aria-label')).toBe('hello');

fixture.componentInstance.ariaLabel = '';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(tabList.hasAttribute('aria-label')).toBe(false);
}));

it('should be able to set the aria-labelledby of the tablist', fakeAsync(() => {
fixture.detectChanges();
tick();

const tabList = fixture.nativeElement.querySelector('.mat-mdc-tab-list') as HTMLElement;
expect(tabList.hasAttribute('aria-labelledby')).toBe(false);

fixture.componentInstance.ariaLabelledby = 'some-label';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(tabList.getAttribute('aria-labelledby')).toBe('some-label');

fixture.componentInstance.ariaLabelledby = '';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
expect(tabList.hasAttribute('aria-labelledby')).toBe(false);
}));
});

describe('aria labelling', () => {
Expand Down Expand Up @@ -1151,6 +1187,8 @@ describe('MatTabNavBar with a default config', () => {
[headerPosition]="headerPosition"
[disableRipple]="disableRipple"
[contentTabIndex]="contentTabIndex"
[aria-label]="ariaLabel"
[aria-labelledby]="ariaLabelledby"
(animationDone)="animationDone()"
(focusChange)="handleFocus($event)"
(selectedTabChange)="handleSelection($event)">
Expand Down Expand Up @@ -1180,6 +1218,8 @@ class SimpleTabsTestApp {
disableRipple: boolean = false;
contentTabIndex: number | null = null;
headerPosition: MatTabHeaderPosition = 'above';
ariaLabel: string;
ariaLabelledby: string;
handleFocus(event: any) {
this.focusEvent = event;
}
Expand Down
6 changes: 6 additions & 0 deletions src/material/tabs/tab-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,12 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes

private _backgroundColor: ThemePalette;

/** Aria label of the inner `tablist` of the group. */
@Input('aria-label') ariaLabel: string;

/** Sets the `aria-labelledby` of the inner `tablist` of the group. */
@Input('aria-labelledby') ariaLabelledby: string;

/** Output to enable support for two-way binding on `[(selectedIndex)]` */
@Output() readonly selectedIndexChange: EventEmitter<number> = new EventEmitter<number>();

Expand Down
2 changes: 2 additions & 0 deletions src/material/tabs/tab-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#tabList
class="mat-mdc-tab-list"
role="tablist"
[attr.aria-label]="ariaLabel || null"
[attr.aria-labelledby]="ariaLabelledby || null"
(cdkObserveContent)="_onContentChanges()">
<div class="mat-mdc-tab-labels" #tabListInner>
<ng-content></ng-content>
Expand Down
6 changes: 6 additions & 0 deletions src/material/tabs/tab-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ export class MatTabHeader
@ViewChild('previousPaginator') _previousPaginator: ElementRef<HTMLElement>;
_inkBar: MatInkBar;

/** Aria label of the header. */
@Input('aria-label') ariaLabel: string;

/** Sets the `aria-labelledby` of the header. */
@Input('aria-labelledby') ariaLabelledby: string;

/** Whether the ripple effect is disabled or not. */
@Input({transform: booleanAttribute})
disableRipple: boolean = false;
Expand Down
8 changes: 6 additions & 2 deletions tools/public_api_guard/material/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,8 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes
set animationDuration(value: string | number);
// (undocumented)
_animationMode?: string | undefined;
ariaLabel: string;
ariaLabelledby: string;
// @deprecated
get backgroundColor(): ThemePalette;
set backgroundColor(value: ThemePalette);
Expand Down Expand Up @@ -320,7 +322,7 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes
_tabs: QueryList<MatTab>;
updatePagination(): void;
// (undocumented)
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>;
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>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatTabGroup, [null, null, { optional: true; }, { optional: true; }]>;
}
Expand All @@ -338,6 +340,8 @@ export interface MatTabGroupBaseHeader {
// @public
export class MatTabHeader extends MatPaginatedTabHeader implements AfterContentChecked, AfterContentInit, AfterViewInit, OnDestroy {
constructor(elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, viewportRuler: ViewportRuler, dir: Directionality, ngZone: NgZone, platform: Platform, animationMode?: string);
ariaLabel: string;
ariaLabelledby: string;
disableRipple: boolean;
// (undocumented)
_inkBar: MatInkBar;
Expand All @@ -360,7 +364,7 @@ export class MatTabHeader extends MatPaginatedTabHeader implements AfterContentC
// (undocumented)
_tabListInner: ElementRef;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatTabHeader, "mat-tab-header", never, { "disableRipple": { "alias": "disableRipple"; "required": false; }; }, {}, ["_items"], ["*"], true, never>;
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>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatTabHeader, [null, null, null, { optional: true; }, null, null, { optional: true; }]>;
}
Expand Down

0 comments on commit f7a0305

Please sign in to comment.