Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit 097b799

Browse files
rschmuklerjelbourn
authored andcommitted
fix(menu): improve aria compliance
closes #4415
1 parent 2a1de83 commit 097b799

File tree

7 files changed

+75
-77
lines changed

7 files changed

+75
-77
lines changed

src/components/menu/js/menuController.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,19 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $r
2525
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
2626
triggerElement = $element[0].querySelector('[ng-click],[ng-mouseenter]');
2727

28-
this.isInMenuBar = opts.isInMenuBar;
2928
this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu'));
3029

3130
menuContainer.on('$mdInterimElementRemove', function() {
3231
self.isOpen = false;
3332
});
3433

34+
var menuContainerId = 'menu_container_' + $mdUtil.nextUid();
35+
menuContainer.attr('id', menuContainerId);
36+
angular.element(triggerElement).attr({
37+
'aria-owns': menuContainerId,
38+
'aria-haspopup': 'true'
39+
});
40+
3541
$scope.$on('$destroy', this.disableHoverListener);
3642
};
3743

@@ -110,8 +116,8 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $r
110116
nestLevel: self.nestLevel,
111117
element: menuContainer,
112118
target: triggerElement,
113-
preserveElement: self.isInMenuBar || self.nestedMenus.length > 0,
114-
parent: self.isInMenuBar ? $element : 'body'
119+
preserveElement: true,
120+
parent: $element
115121
}).finally(function() {
116122
self.disableHoverListener();
117123
});
@@ -123,12 +129,14 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $r
123129
$scope.$watch(function() { return self.isOpen; }, function(isOpen) {
124130
if (isOpen) {
125131
triggerElement.setAttribute('aria-expanded', 'true');
132+
menuContainer.attr('aria-hidden', 'false');
126133
$element[0].classList.add('md-open');
127134
angular.forEach(self.nestedMenus, function(el) {
128135
el.classList.remove('md-open');
129136
});
130137
} else {
131138
triggerElement && triggerElement.setAttribute('aria-expanded', 'false');
139+
menuContainer.attr('aria-hidden', 'true');
132140
$element[0].classList.remove('md-open');
133141
}
134142
$scope.$mdMenuIsOpen = self.isOpen;

src/components/menu/js/menuDirective.js

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,6 @@ function MenuDirective($mdUtil) {
186186
}
187187
menuEl.classList.add('md-nested-menu');
188188
menuEl.setAttribute('md-nest-level', nestingDepth + 1);
189-
menuEl.setAttribute('role', 'menu');
190189
});
191190
}
192191
return link;
@@ -200,20 +199,13 @@ function MenuDirective($mdUtil) {
200199
'<div class="md-open-menu-container md-whiteframe-z2"></div>'
201200
);
202201
var menuContents = element.children()[1];
203-
menuContainer.append(menuContents);
204-
if (isInMenuBar) {
205-
element.append(menuContainer);
206-
menuContainer[0].style.display = 'none';
202+
if (!menuContents.hasAttribute('role')) {
203+
menuContents.setAttribute('role', 'menu');
207204
}
208-
mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar });
209-
210-
scope.$on('$destroy', function() {
211-
mdMenuCtrl
212-
.destroy()
213-
.finally(function(){
214-
menuContainer.remove();
215-
});
216-
});
205+
menuContainer.append(menuContents);
217206

207+
element.append(menuContainer);
208+
menuContainer[0].style.display = 'none';
209+
mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar });
218210
}
219211
}

src/components/menu/js/menuServiceProvider.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function MenuProvider($$interimElementProvider) {
5656
if (options.hasBackdrop) {
5757
options.backdrop = $mdUtil.createBackdrop(scope, "md-menu-backdrop md-click-catcher");
5858

59-
$animate.enter(options.backdrop, options.parent);
59+
$animate.enter(options.backdrop, $document[0].body);
6060
}
6161

6262
/**
@@ -294,7 +294,7 @@ function MenuProvider($$interimElementProvider) {
294294
if ((hasAnyAttribute(target, ['ng-click', 'ng-href', 'ui-sref']) ||
295295
target.nodeName == 'BUTTON' || target.nodeName == 'MD-BUTTON') && !hasAnyAttribute(target, ['md-prevent-menu-close'])) {
296296
var closestMenu = $mdUtil.getClosest(target, 'MD-MENU');
297-
if (!target.hasAttribute('disabled') && (!closestMenu || closestMenu == opts.parent[0])) {
297+
if (!target.hasAttribute('disabled') && (closestMenu == opts.parent[0])) {
298298
close();
299299
}
300300
break;

src/components/menu/menu.spec.js

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,6 @@ describe('material.components.menu', function() {
2828
expect(buildBadMenu).toThrow();
2929
}));
3030

31-
it('removes everything but the first element', function() {
32-
var menu = setup()[0];
33-
expect(menu.children.length).toBe(1);
34-
expect(menu.firstElementChild.nodeName).toBe('BUTTON');
35-
});
36-
3731
it('specifies button type', inject(function($compile, $rootScope) {
3832
var menu = setup()[0];
3933
expect(menu.firstElementChild.getAttribute('type')).toBe('button');
@@ -42,35 +36,35 @@ describe('material.components.menu', function() {
4236
it('opens on click', function () {
4337
var menu = setup();
4438
openMenu(menu);
45-
expect(getOpenMenuContainer().length).toBe(1);
39+
expect(getOpenMenuContainer(menu).length).toBe(1);
4640
closeMenu(menu);
47-
expect(getOpenMenuContainer().length).toBe(0);
41+
expect(getOpenMenuContainer(menu).length).toBe(0);
4842
});
4943

5044
it('opens on click without $event', function() {
5145
var noEvent = true;
5246
var menu = setup('ng-click', noEvent);
5347
openMenu(menu);
54-
expect(getOpenMenuContainer().length).toBe(1);
48+
expect(getOpenMenuContainer(menu).length).toBe(1);
5549
closeMenu(menu);
56-
expect(getOpenMenuContainer().length).toBe(0);
50+
expect(getOpenMenuContainer(menu).length).toBe(0);
5751
});
5852

5953
it('opens on mouseEnter', function() {
6054
var menu = setup('ng-mouseenter');
6155
openMenu(menu, 'mouseenter');
62-
expect(getOpenMenuContainer().length).toBe(1);
56+
expect(getOpenMenuContainer(menu).length).toBe(1);
6357
closeMenu(menu);
64-
expect(getOpenMenuContainer().length).toBe(0);
58+
expect(getOpenMenuContainer(menu).length).toBe(0);
6559
});
6660

6761
it('opens on mouseEnter without $event', function() {
6862
var noEvent = true;
6963
var menu = setup('ng-mouseenter', noEvent);
7064
openMenu(menu, 'mouseenter');
71-
expect(getOpenMenuContainer().length).toBe(1);
65+
expect(getOpenMenuContainer(menu).length).toBe(1);
7266
closeMenu(menu);
73-
expect(getOpenMenuContainer().length).toBe(0);
67+
expect(getOpenMenuContainer(menu).length).toBe(0);
7468
});
7569

7670
it('should not propagate the click event', function() {
@@ -87,56 +81,49 @@ describe('material.components.menu', function() {
8781

8882
it('closes on backdrop click', inject(function($document) {
8983

90-
openMenu(setup());
84+
var menu = setup();
85+
openMenu(menu);
9186

92-
expect(getOpenMenuContainer().length).toBe(1);
87+
expect(getOpenMenuContainer(menu).length).toBe(1);
9388

9489
$document.find('md-backdrop').triggerHandler('click');
9590
waitForMenuClose();
9691

97-
expect(getOpenMenuContainer().length).toBe(0);
92+
expect(getOpenMenuContainer(menu).length).toBe(0);
9893
}));
9994

10095

10196
it('closes on escape', inject(function($document, $mdConstant) {
102-
openMenu(setup());
103-
expect(getOpenMenuContainer().length).toBe(1);
97+
var menu = setup();
98+
openMenu(menu);
99+
expect(getOpenMenuContainer(menu).length).toBe(1);
104100

105-
var openMenuEl = $document[0].querySelector('md-menu-content');
101+
var openMenuEl = menu[0].querySelector('md-menu-content');
106102

107103
pressKey(openMenuEl, $mdConstant.KEY_CODE.ESCAPE);
108104
waitForMenuClose();
109105

110-
expect(getOpenMenuContainer().length).toBe(0);
111-
}));
112-
113-
it('closes on $destroy', inject(function($document, $rootScope) {
114-
var scope = $rootScope.$new();
115-
openMenu( setup(null,false,scope) );
116-
117-
expect(getOpenMenuContainer().length).toBe(1);
118-
scope.$destroy();
119-
120-
expect(getOpenMenuContainer().length).toBe(0);
106+
expect(getOpenMenuContainer(menu).length).toBe(0);
121107
}));
122108

123109
describe('closes with -', function() {
124110
it('closes on normal option click', function() {
125-
expect(getOpenMenuContainer().length).toBe(0);
126111

127-
openMenu(setup());
112+
var menu = setup();
113+
expect(getOpenMenuContainer(menu).length).toBe(0);
114+
openMenu(menu);
128115

129116
expect(menuActionPerformed).toBeFalsy();
130-
expect(getOpenMenuContainer().length).toBe(1);
117+
expect(getOpenMenuContainer(menu).length).toBe(1);
131118

132-
var btn = getOpenMenuContainer()[0].querySelector('md-button');
119+
var btn = getOpenMenuContainer(menu)[0].querySelector('md-button');
133120
btn.click();
134121

135122
waitForMenuClose();
136123

137124
expect(menuActionPerformed).toBeTruthy();
138125

139-
expect(getOpenMenuContainer().length).toBe(0);
126+
expect(getOpenMenuContainer(menu).length).toBe(0);
140127
});
141128

142129
itClosesWithAttributes([
@@ -151,7 +138,7 @@ describe('material.components.menu', function() {
151138
}
152139

153140
function testAttribute(attr) {
154-
return inject(function($rootScope, $compile, $timeout, $browser, $animate) {
141+
return inject(function($rootScope, $compile, $timeout, $browser) {
155142
var template = '' +
156143
'<md-menu>' +
157144
' <button ng-click="$mdOpenMenu($event)">Hello World</button>' +
@@ -163,17 +150,18 @@ describe('material.components.menu', function() {
163150
'</md-menu>';
164151

165152

166-
openMenu($compile(template)($rootScope));
153+
var menu = $compile(template)($rootScope);
154+
openMenu(menu);
167155

168-
expect(getOpenMenuContainer().length).toBe(1);
156+
expect(getOpenMenuContainer(menu).length).toBe(1);
169157

170158
$timeout.flush();
171-
var btn = getOpenMenuContainer()[0].querySelector('md-button');
159+
var btn = getOpenMenuContainer(menu)[0].querySelector('md-button');
172160
btn.click();
173161

174162
waitForMenuClose();
175163

176-
expect(getOpenMenuContainer().length).toBe(0);
164+
expect(getOpenMenuContainer(menu).length).toBe(0);
177165
});
178166
}
179167
}
@@ -208,21 +196,27 @@ describe('material.components.menu', function() {
208196
// Internal methods
209197
// ********************************************
210198

211-
function getOpenMenuContainer() {
212-
var res;
213-
inject(function($document) {
214-
res = angular.element($document[0].querySelector('.md-open-menu-container'));
215-
});
216-
return res;
199+
function getOpenMenuContainer(el) {
200+
el = (el instanceof angular.element) ? el[0] : el;
201+
var container = el.querySelector('.md-open-menu-container');
202+
if (container.style.display == 'none') {
203+
return angular.element([]);
204+
} else {
205+
return angular.element(container);
206+
}
217207
}
218208

219209
function openMenu(el, triggerType) {
220-
el.children().eq(0).triggerHandler(triggerType || 'click');
221-
waitForMenuOpen();
210+
inject(function($document) {
211+
el.children().eq(0).triggerHandler(triggerType || 'click');
212+
$document[0].body.appendChild(el[0]);
213+
waitForMenuOpen();
214+
});
222215
}
223216

224217
function closeMenu() {
225218
inject(function($document) {
219+
$document.find('md-backdrop');
226220
$document.find('md-backdrop').triggerHandler('click');
227221
waitForMenuClose();
228222
});

src/components/menuBar/js/menuBarDirective.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,8 @@ function MenuBarDirective($mdUtil, $mdTheming) {
110110
if (menuEl.nodeName == 'MD-MENU') {
111111
if (!menuEl.hasAttribute('md-position-mode')) {
112112
menuEl.setAttribute('md-position-mode', 'left bottom');
113+
menuEl.querySelector('button,a').setAttribute('role', 'menuitem');
113114
}
114-
menuEl.setAttribute('role', 'menu');
115115
var contentEls = $mdUtil.nodesToArray(menuEl.querySelectorAll('md-menu-content'));
116116
angular.forEach(contentEls, function(contentEl) {
117117
contentEl.classList.add('md-menu-bar-menu');

src/components/menuBar/js/menuItemDirective.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ function MenuItemDirective() {
2222
templateEl.append(buttonEl);
2323
templateEl[0].classList.add('md-indent');
2424

25-
setDefault('role', (templateAttrs.type == 'checkbox') ? 'menuitemcheckbox' : 'menuitemradio');
25+
setDefault('role', (templateAttrs.type == 'checkbox') ? 'menuitemcheckbox' : 'menuitemradio', buttonEl);
2626
angular.forEach(['ng-disabled'], moveAttrToButton);
2727

2828
} else {
29-
setDefault('role', 'menuitem');
29+
setDefault('role', 'menuitem', templateEl[0].querySelector('md-button,button,a'));
3030
}
3131

3232

@@ -36,9 +36,13 @@ function MenuItemDirective() {
3636
ctrl.init(ngModel);
3737
};
3838

39-
function setDefault(attr, val) {
40-
if (!templateEl[0].hasAttribute(attr)) {
41-
templateEl[0].setAttribute(attr, val);
39+
function setDefault(attr, val, el) {
40+
el = el || templateEl;
41+
if (el instanceof angular.element) {
42+
el = el[0];
43+
}
44+
if (!el.hasAttribute(attr)) {
45+
el.setAttribute(attr, val);
4246
}
4347
}
4448

src/components/menuBar/menu-bar.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe('material.components.menuBar', function() {
1919
});
2020

2121
describe('ARIA', function() {
22-
it('sets aria-role="menubar" on the menubar', function() {
22+
it('sets role="menubar" on the menubar', function() {
2323
var menuBar = setup();
2424
var ariaRole = menuBar[0].getAttribute('role');
2525
expect(ariaRole).toBe('menubar');
@@ -207,7 +207,7 @@ describe('material.components.menuBar', function() {
207207
expect(children[1].nodeName).toBe('MD-BUTTON');
208208
});
209209
it('sets aria role', function() {
210-
var menuItem = setup()[0];
210+
var menuItem = setup()[0].querySelector('md-button');
211211
expect(menuItem.getAttribute('role')).toBe('menuitemcheckbox');
212212
});
213213
it('toggles on click', function() {
@@ -259,7 +259,7 @@ describe('material.components.menuBar', function() {
259259
expect(children[1].nodeName).toBe('MD-BUTTON');
260260
});
261261
it('sets aria role', function() {
262-
var menuItem = setup()[0];
262+
var menuItem = setup()[0].querySelector('md-button');
263263
expect(menuItem.getAttribute('role')).toBe('menuitemradio');
264264
});
265265
it('toggles on click', function() {

0 commit comments

Comments
 (0)