Skip to content

Commit 3299d8c

Browse files
crisbetoandrewseguin
authored andcommitted
fix(menu): forward aria attribute to menu panel (#17957)
Forwards the `aria-label`, `aria-labelledby` and `aria-describedby` attributes from the menu host element to the menu panel inside the overlay. Fixes #17952.
1 parent 050ee7a commit 3299d8c

File tree

6 files changed

+99
-5
lines changed

6 files changed

+99
-5
lines changed

src/material-experimental/mdc-menu/menu.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
(@transformMenu.start)="_onAnimationStart($event)"
1010
(@transformMenu.done)="_onAnimationDone($event)"
1111
tabindex="-1"
12-
role="menu">
12+
role="menu"
13+
[attr.aria-label]="ariaLabel || null"
14+
[attr.aria-labelledby]="ariaLabelledby || null"
15+
[attr.aria-describedby]="ariaDescribedby || null">
1316
<div class="mat-mdc-menu-content mdc-list">
1417
<ng-content></ng-content>
1518
</div>

src/material-experimental/mdc-menu/menu.spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,38 @@ describe('MDC-based MatMenu', () => {
503503
expect(role).toBe('menu', 'Expected panel to have the "menu" role.');
504504
});
505505

506+
it('should forward ARIA attributes to the menu panel', () => {
507+
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
508+
const instance = fixture.componentInstance;
509+
fixture.detectChanges();
510+
instance.trigger.openMenu();
511+
fixture.detectChanges();
512+
513+
const menuPanel = overlayContainerElement.querySelector('.mat-mdc-menu-panel')!;
514+
expect(menuPanel.hasAttribute('aria-label')).toBe(false);
515+
expect(menuPanel.hasAttribute('aria-labelledby')).toBe(false);
516+
expect(menuPanel.hasAttribute('aria-describedby')).toBe(false);
517+
518+
// Note that setting all of these at the same time is invalid,
519+
// but it's up to the consumer to handle it correctly.
520+
instance.ariaLabel = 'Custom aria-label';
521+
instance.ariaLabelledby = 'custom-labelled-by';
522+
instance.ariaDescribedby = 'custom-described-by';
523+
fixture.detectChanges();
524+
525+
expect(menuPanel.getAttribute('aria-label')).toBe('Custom aria-label');
526+
expect(menuPanel.getAttribute('aria-labelledby')).toBe('custom-labelled-by');
527+
expect(menuPanel.getAttribute('aria-describedby')).toBe('custom-described-by');
528+
529+
// Change these to empty strings to make sure that we don't preserve empty attributes.
530+
instance.ariaLabel = instance.ariaLabelledby = instance.ariaDescribedby = '';
531+
fixture.detectChanges();
532+
533+
expect(menuPanel.hasAttribute('aria-label')).toBe(false);
534+
expect(menuPanel.hasAttribute('aria-labelledby')).toBe(false);
535+
expect(menuPanel.hasAttribute('aria-describedby')).toBe(false);
536+
});
537+
506538
it('should set the "menuitem" role on the items by default', () => {
507539
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
508540
fixture.detectChanges();
@@ -2163,7 +2195,10 @@ describe('MatMenu default overrides', () => {
21632195
#menu="matMenu"
21642196
[class]="panelClass"
21652197
(closed)="closeCallback($event)"
2166-
[backdropClass]="backdropClass">
2198+
[backdropClass]="backdropClass"
2199+
[aria-label]="ariaLabel"
2200+
[aria-labelledby]="ariaLabelledby"
2201+
[aria-describedby]="ariaDescribedby">
21672202
21682203
<button mat-menu-item> Item </button>
21692204
<button mat-menu-item disabled> Disabled </button>
@@ -2185,6 +2220,9 @@ class SimpleMenu {
21852220
backdropClass: string;
21862221
panelClass: string;
21872222
restoreFocus = true;
2223+
ariaLabel: string;
2224+
ariaLabelledby: string;
2225+
ariaDescribedby: string;
21882226
}
21892227

21902228
@Component({

src/material/menu/menu.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
(@transformMenu.start)="_onAnimationStart($event)"
1010
(@transformMenu.done)="_onAnimationDone($event)"
1111
tabindex="-1"
12-
role="menu">
12+
role="menu"
13+
[attr.aria-label]="ariaLabel || null"
14+
[attr.aria-labelledby]="ariaLabelledby || null"
15+
[attr.aria-describedby]="ariaDescribedby || null">
1316
<div class="mat-menu-content">
1417
<ng-content></ng-content>
1518
</div>

src/material/menu/menu.spec.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,38 @@ describe('MatMenu', () => {
503503
expect(role).toBe('menu', 'Expected panel to have the "menu" role.');
504504
});
505505

506+
it('should forward ARIA attributes to the menu panel', () => {
507+
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
508+
const instance = fixture.componentInstance;
509+
fixture.detectChanges();
510+
instance.trigger.openMenu();
511+
fixture.detectChanges();
512+
513+
const menuPanel = overlayContainerElement.querySelector('.mat-menu-panel')!;
514+
expect(menuPanel.hasAttribute('aria-label')).toBe(false);
515+
expect(menuPanel.hasAttribute('aria-labelledby')).toBe(false);
516+
expect(menuPanel.hasAttribute('aria-describedby')).toBe(false);
517+
518+
// Note that setting all of these at the same time is invalid,
519+
// but it's up to the consumer to handle it correctly.
520+
instance.ariaLabel = 'Custom aria-label';
521+
instance.ariaLabelledby = 'custom-labelled-by';
522+
instance.ariaDescribedby = 'custom-described-by';
523+
fixture.detectChanges();
524+
525+
expect(menuPanel.getAttribute('aria-label')).toBe('Custom aria-label');
526+
expect(menuPanel.getAttribute('aria-labelledby')).toBe('custom-labelled-by');
527+
expect(menuPanel.getAttribute('aria-describedby')).toBe('custom-described-by');
528+
529+
// Change these to empty strings to make sure that we don't preserve empty attributes.
530+
instance.ariaLabel = instance.ariaLabelledby = instance.ariaDescribedby = '';
531+
fixture.detectChanges();
532+
533+
expect(menuPanel.hasAttribute('aria-label')).toBe(false);
534+
expect(menuPanel.hasAttribute('aria-labelledby')).toBe(false);
535+
expect(menuPanel.hasAttribute('aria-describedby')).toBe(false);
536+
});
537+
506538
it('should set the "menuitem" role on the items by default', () => {
507539
const fixture = createComponent(SimpleMenu, [], [FakeIcon]);
508540
fixture.detectChanges();
@@ -2151,7 +2183,10 @@ describe('MatMenu default overrides', () => {
21512183
#menu="matMenu"
21522184
[class]="panelClass"
21532185
(closed)="closeCallback($event)"
2154-
[backdropClass]="backdropClass">
2186+
[backdropClass]="backdropClass"
2187+
[aria-label]="ariaLabel"
2188+
[aria-labelledby]="ariaLabelledby"
2189+
[aria-describedby]="ariaDescribedby">
21552190
21562191
<button mat-menu-item> Item </button>
21572192
<button mat-menu-item disabled> Disabled </button>
@@ -2173,6 +2208,9 @@ class SimpleMenu {
21732208
backdropClass: string;
21742209
panelClass: string;
21752210
restoreFocus = true;
2211+
ariaLabel: string;
2212+
ariaLabelledby: string;
2213+
ariaDescribedby: string;
21762214
}
21772215

21782216
@Component({

src/material/menu/menu.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,15 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
132132
/** Class to be added to the backdrop element. */
133133
@Input() backdropClass: string = this._defaultOptions.backdropClass;
134134

135+
/** aria-label for the menu panel. */
136+
@Input('aria-label') ariaLabel: string;
137+
138+
/** aria-labelledby for the menu panel. */
139+
@Input('aria-labelledby') ariaLabelledby: string;
140+
141+
/** aria-describedby for the menu panel. */
142+
@Input('aria-describedby') ariaDescribedby: string;
143+
135144
/** Position of the menu in the X axis. */
136145
@Input()
137146
get xPosition(): MenuPositionX { return this._xPosition; }

tools/public_api_guard/material/menu.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatM
1414
};
1515
_isAnimating: boolean;
1616
_panelAnimationState: 'void' | 'enter';
17+
ariaDescribedby: string;
18+
ariaLabel: string;
19+
ariaLabelledby: string;
1720
backdropClass: string;
1821
classList: string;
1922
close: EventEmitter<void | 'click' | 'keydown' | 'tab'>;
@@ -45,7 +48,7 @@ export declare class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatM
4548
resetActiveItem(): void;
4649
setElevation(depth: number): void;
4750
setPositionClasses(posX?: MenuPositionX, posY?: MenuPositionY): void;
48-
static ɵdir: i0.ɵɵDirectiveDefWithMeta<_MatMenuBase, never, never, { 'backdropClass': "backdropClass", 'xPosition': "xPosition", 'yPosition': "yPosition", 'overlapTrigger': "overlapTrigger", 'hasBackdrop': "hasBackdrop", 'panelClass': "class", 'classList': "classList" }, { 'closed': "closed", 'close': "close" }, ["lazyContent", "_allItems", "items"]>;
51+
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"]>;
4952
static ɵfac: i0.ɵɵFactoryDef<_MatMenuBase>;
5053
}
5154

0 commit comments

Comments
 (0)