From b21131a6693af3990494d02d420c845c27e53c76 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Fri, 13 Dec 2019 09:48:24 +0100 Subject: [PATCH] fix(menu): forward aria attribute to menu panel Forwards the `aria-label`, `aria-labelledby` and `aria-describedby` attributes from the menu host element to the menu panel inside the overlay. Fixes #17952. --- src/material-experimental/mdc-menu/menu.html | 5 ++- .../mdc-menu/menu.spec.ts | 40 ++++++++++++++++++- src/material/menu/menu.html | 5 ++- src/material/menu/menu.spec.ts | 40 ++++++++++++++++++- src/material/menu/menu.ts | 9 +++++ tools/public_api_guard/material/menu.d.ts | 5 ++- 6 files changed, 99 insertions(+), 5 deletions(-) diff --git a/src/material-experimental/mdc-menu/menu.html b/src/material-experimental/mdc-menu/menu.html index c2c2c285c999..90d8bcc58f64 100644 --- a/src/material-experimental/mdc-menu/menu.html +++ b/src/material-experimental/mdc-menu/menu.html @@ -9,7 +9,10 @@ (@transformMenu.start)="_onAnimationStart($event)" (@transformMenu.done)="_onAnimationDone($event)" tabindex="-1" - role="menu"> + role="menu" + [attr.aria-label]="ariaLabel || null" + [attr.aria-labelledby]="ariaLabelledby || null" + [attr.aria-describedby]="ariaDescribedby || null">
diff --git a/src/material-experimental/mdc-menu/menu.spec.ts b/src/material-experimental/mdc-menu/menu.spec.ts index 7d983727a6db..83dfee6e0bbb 100644 --- a/src/material-experimental/mdc-menu/menu.spec.ts +++ b/src/material-experimental/mdc-menu/menu.spec.ts @@ -503,6 +503,38 @@ describe('MDC-based MatMenu', () => { expect(role).toBe('menu', 'Expected panel to have the "menu" role.'); }); + it('should forward ARIA attributes to the menu panel', () => { + const fixture = createComponent(SimpleMenu, [], [FakeIcon]); + const instance = fixture.componentInstance; + fixture.detectChanges(); + instance.trigger.openMenu(); + fixture.detectChanges(); + + const menuPanel = overlayContainerElement.querySelector('.mat-mdc-menu-panel')!; + expect(menuPanel.hasAttribute('aria-label')).toBe(false); + expect(menuPanel.hasAttribute('aria-labelledby')).toBe(false); + expect(menuPanel.hasAttribute('aria-describedby')).toBe(false); + + // Note that setting all of these at the same time is invalid, + // but it's up to the consumer to handle it correctly. + instance.ariaLabel = 'Custom aria-label'; + instance.ariaLabelledby = 'custom-labelled-by'; + instance.ariaDescribedby = 'custom-described-by'; + fixture.detectChanges(); + + expect(menuPanel.getAttribute('aria-label')).toBe('Custom aria-label'); + expect(menuPanel.getAttribute('aria-labelledby')).toBe('custom-labelled-by'); + expect(menuPanel.getAttribute('aria-describedby')).toBe('custom-described-by'); + + // Change these to empty strings to make sure that we don't preserve empty attributes. + instance.ariaLabel = instance.ariaLabelledby = instance.ariaDescribedby = ''; + fixture.detectChanges(); + + expect(menuPanel.hasAttribute('aria-label')).toBe(false); + expect(menuPanel.hasAttribute('aria-labelledby')).toBe(false); + expect(menuPanel.hasAttribute('aria-describedby')).toBe(false); + }); + it('should set the "menuitem" role on the items by default', () => { const fixture = createComponent(SimpleMenu, [], [FakeIcon]); fixture.detectChanges(); @@ -2163,7 +2195,10 @@ describe('MatMenu default overrides', () => { #menu="matMenu" [class]="panelClass" (closed)="closeCallback($event)" - [backdropClass]="backdropClass"> + [backdropClass]="backdropClass" + [aria-label]="ariaLabel" + [aria-labelledby]="ariaLabelledby" + [aria-describedby]="ariaDescribedby"> @@ -2185,6 +2220,9 @@ class SimpleMenu { backdropClass: string; panelClass: string; restoreFocus = true; + ariaLabel: string; + ariaLabelledby: string; + ariaDescribedby: string; } @Component({ diff --git a/src/material/menu/menu.html b/src/material/menu/menu.html index acd80470095e..8bd298ce6417 100644 --- a/src/material/menu/menu.html +++ b/src/material/menu/menu.html @@ -9,7 +9,10 @@ (@transformMenu.start)="_onAnimationStart($event)" (@transformMenu.done)="_onAnimationDone($event)" tabindex="-1" - role="menu"> + role="menu" + [attr.aria-label]="ariaLabel || null" + [attr.aria-labelledby]="ariaLabelledby || null" + [attr.aria-describedby]="ariaDescribedby || null">
diff --git a/src/material/menu/menu.spec.ts b/src/material/menu/menu.spec.ts index 1b0af4d27f80..2b459397f09e 100644 --- a/src/material/menu/menu.spec.ts +++ b/src/material/menu/menu.spec.ts @@ -503,6 +503,38 @@ describe('MatMenu', () => { expect(role).toBe('menu', 'Expected panel to have the "menu" role.'); }); + it('should forward ARIA attributes to the menu panel', () => { + const fixture = createComponent(SimpleMenu, [], [FakeIcon]); + const instance = fixture.componentInstance; + fixture.detectChanges(); + instance.trigger.openMenu(); + fixture.detectChanges(); + + const menuPanel = overlayContainerElement.querySelector('.mat-menu-panel')!; + expect(menuPanel.hasAttribute('aria-label')).toBe(false); + expect(menuPanel.hasAttribute('aria-labelledby')).toBe(false); + expect(menuPanel.hasAttribute('aria-describedby')).toBe(false); + + // Note that setting all of these at the same time is invalid, + // but it's up to the consumer to handle it correctly. + instance.ariaLabel = 'Custom aria-label'; + instance.ariaLabelledby = 'custom-labelled-by'; + instance.ariaDescribedby = 'custom-described-by'; + fixture.detectChanges(); + + expect(menuPanel.getAttribute('aria-label')).toBe('Custom aria-label'); + expect(menuPanel.getAttribute('aria-labelledby')).toBe('custom-labelled-by'); + expect(menuPanel.getAttribute('aria-describedby')).toBe('custom-described-by'); + + // Change these to empty strings to make sure that we don't preserve empty attributes. + instance.ariaLabel = instance.ariaLabelledby = instance.ariaDescribedby = ''; + fixture.detectChanges(); + + expect(menuPanel.hasAttribute('aria-label')).toBe(false); + expect(menuPanel.hasAttribute('aria-labelledby')).toBe(false); + expect(menuPanel.hasAttribute('aria-describedby')).toBe(false); + }); + it('should set the "menuitem" role on the items by default', () => { const fixture = createComponent(SimpleMenu, [], [FakeIcon]); fixture.detectChanges(); @@ -2151,7 +2183,10 @@ describe('MatMenu default overrides', () => { #menu="matMenu" [class]="panelClass" (closed)="closeCallback($event)" - [backdropClass]="backdropClass"> + [backdropClass]="backdropClass" + [aria-label]="ariaLabel" + [aria-labelledby]="ariaLabelledby" + [aria-describedby]="ariaDescribedby"> @@ -2173,6 +2208,9 @@ class SimpleMenu { backdropClass: string; panelClass: string; restoreFocus = true; + ariaLabel: string; + ariaLabelledby: string; + ariaDescribedby: string; } @Component({ diff --git a/src/material/menu/menu.ts b/src/material/menu/menu.ts index ec48f2a2d2e1..4333955bac47 100644 --- a/src/material/menu/menu.ts +++ b/src/material/menu/menu.ts @@ -132,6 +132,15 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel /** Class to be added to the backdrop element. */ @Input() backdropClass: string = this._defaultOptions.backdropClass; + /** aria-label for the menu panel. */ + @Input('aria-label') ariaLabel: string; + + /** aria-labelledby for the menu panel. */ + @Input('aria-labelledby') ariaLabelledby: string; + + /** aria-describedby for the menu panel. */ + @Input('aria-describedby') ariaDescribedby: string; + /** Position of the menu in the X axis. */ @Input() get xPosition(): MenuPositionX { return this._xPosition; } diff --git a/tools/public_api_guard/material/menu.d.ts b/tools/public_api_guard/material/menu.d.ts index b3e4a75fb345..90393bc89156 100644 --- a/tools/public_api_guard/material/menu.d.ts +++ b/tools/public_api_guard/material/menu.d.ts @@ -14,6 +14,9 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel; @@ -45,7 +48,7 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel; + static ɵdir: i0.ɵɵDirectiveDefWithMeta<_MatMenuBase, never, never, { 'backdropClass': "backdropClass", 'ariaLabel': "aria-label", 'ariaLabelledby': "aria-labelledby", 'ariaDescribedby': "aria-describedby", 'xPosition': "xPosition", 'yPosition': "yPosition", 'overlapTrigger': "overlapTrigger", 'hasBackdrop': "hasBackdrop", 'panelClass': "class", 'classList': "classList" }, { 'closed': "closed", 'close': "close" }, ["lazyContent", "_allItems", "items"]>; static ɵfac: i0.ɵɵFactoryDef<_MatMenuBase>; }