From f7a03056fd37ce19f70fbed987efb83e9eebd70a Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 14 Aug 2024 16:29:01 +0200 Subject: [PATCH] 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 1968cc4e0ffeec12e7067318fc2391fcf5b59dfc) --- src/material/tabs/tab-group.html | 2 ++ src/material/tabs/tab-group.spec.ts | 40 +++++++++++++++++++++++++ src/material/tabs/tab-group.ts | 6 ++++ src/material/tabs/tab-header.html | 2 ++ src/material/tabs/tab-header.ts | 6 ++++ tools/public_api_guard/material/tabs.md | 8 +++-- 6 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/material/tabs/tab-group.html b/src/material/tabs/tab-group.html index 4bdc7e484614..1208762159dd 100644 --- a/src/material/tabs/tab-group.html +++ b/src/material/tabs/tab-group.html @@ -2,6 +2,8 @@ [selectedIndex]="selectedIndex || 0" [disableRipple]="disableRipple" [disablePagination]="disablePagination" + [aria-label]="ariaLabel" + [aria-labelledby]="ariaLabelledby" (indexFocused)="_focusChanged($event)" (selectFocusedIndex)="selectedIndex = $event"> diff --git a/src/material/tabs/tab-group.spec.ts b/src/material/tabs/tab-group.spec.ts index 2633ebe2e524..bb37b3735443 100644 --- a/src/material/tabs/tab-group.spec.ts +++ b/src/material/tabs/tab-group.spec.ts @@ -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', () => { @@ -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)"> @@ -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; } diff --git a/src/material/tabs/tab-group.ts b/src/material/tabs/tab-group.ts index d4c0049566ea..84c7695ca30d 100644 --- a/src/material/tabs/tab-group.ts +++ b/src/material/tabs/tab-group.ts @@ -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 = new EventEmitter(); diff --git a/src/material/tabs/tab-header.html b/src/material/tabs/tab-header.html index aca77b31e022..4c2e5772999e 100644 --- a/src/material/tabs/tab-header.html +++ b/src/material/tabs/tab-header.html @@ -22,6 +22,8 @@ #tabList class="mat-mdc-tab-list" role="tablist" + [attr.aria-label]="ariaLabel || null" + [attr.aria-labelledby]="ariaLabelledby || null" (cdkObserveContent)="_onContentChanges()">
diff --git a/src/material/tabs/tab-header.ts b/src/material/tabs/tab-header.ts index da88094f4554..d262948ac661 100644 --- a/src/material/tabs/tab-header.ts +++ b/src/material/tabs/tab-header.ts @@ -69,6 +69,12 @@ export class MatTabHeader @ViewChild('previousPaginator') _previousPaginator: ElementRef; _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; diff --git a/tools/public_api_guard/material/tabs.md b/tools/public_api_guard/material/tabs.md index d911304e52e4..5b542f8c44bb 100644 --- a/tools/public_api_guard/material/tabs.md +++ b/tools/public_api_guard/material/tabs.md @@ -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); @@ -320,7 +322,7 @@ export class MatTabGroup implements AfterContentInit, AfterContentChecked, OnDes _tabs: QueryList; updatePagination(): void; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; } @@ -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; @@ -360,7 +364,7 @@ export class MatTabHeader extends MatPaginatedTabHeader implements AfterContentC // (undocumented) _tabListInner: ElementRef; // (undocumented) - static ɵcmp: i0.ɵɵComponentDeclaration; + static ɵcmp: i0.ɵɵComponentDeclaration; // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; }