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

Commit eb1695a

Browse files
committed
fix(menubar): fix broken menubar accessability with checkbox and radio menuitems
closes #6151
1 parent 378248a commit eb1695a

File tree

6 files changed

+34
-11
lines changed

6 files changed

+34
-11
lines changed

src/components/menu/js/menuController.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $r
2424
menuContainer = setMenuContainer;
2525
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
2626
triggerElement = $element[0].querySelector('[ng-click],[ng-mouseenter]');
27+
triggerElement.setAttribute('aria-expanded', 'false');
2728

2829
this.isInMenuBar = opts.isInMenuBar;
2930
this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu'));
@@ -110,6 +111,7 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $r
110111
self.enableHoverListener();
111112
self.isOpen = true;
112113
triggerElement = triggerElement || (ev ? ev.target : $element[0]);
114+
triggerElement.setAttribute('aria-expanded', 'true');
113115
$scope.$emit('$mdMenuOpen', $element);
114116
$mdMenu.show({
115117
scope: $scope,
@@ -120,6 +122,7 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $r
120122
preserveElement: true,
121123
parent: 'body'
122124
}).finally(function() {
125+
triggerElement.setAttribute('aria-expanded', 'false');
123126
self.disableHoverListener();
124127
});
125128
};

src/components/menu/js/menuServiceProvider.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,13 +238,13 @@ function MenuProvider($$interimElementProvider) {
238238
handled = true;
239239
break;
240240
case $mdConstant.KEY_CODE.UP_ARROW:
241-
if (!focusMenuItem(ev, opts.menuContentEl, opts, -1)) {
241+
if (!focusMenuItem(ev, opts.menuContentEl, opts, -1) && !opts.nestLevel) {
242242
opts.mdMenuCtrl.triggerContainerProxy(ev);
243243
}
244244
handled = true;
245245
break;
246246
case $mdConstant.KEY_CODE.DOWN_ARROW:
247-
if (!focusMenuItem(ev, opts.menuContentEl, opts, 1)) {
247+
if (!focusMenuItem(ev, opts.menuContentEl, opts, 1) && !opts.nestLevel) {
248248
opts.mdMenuCtrl.triggerContainerProxy(ev);
249249
}
250250
handled = true;

src/components/menu/menu.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
ddescribe('material.components.menu', function() {
1+
describe('material.components.menu', function() {
22
var attachedElements = [];
33
var $mdMenu, $timeout, menuActionPerformed, $mdUtil;
44

src/components/menuBar/js/menuItemController.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,27 @@ MenuItemController.prototype.init = function(ngModel) {
2222
this.mode = $attrs.type;
2323
this.iconEl = $element[0].children[0];
2424
this.buttonEl = $element[0].children[1];
25-
if (ngModel) this.initClickListeners();
25+
if (ngModel) {
26+
// Clear ngAria set attributes
27+
this.initClickListeners();
28+
}
2629
}
2730
};
2831

32+
// ngAria auto sets attributes on a menu-item with a ngModel.
33+
// We don't want this because our content (buttons) get the focus
34+
// and set their own aria attributes appropritately. Having both
35+
// breaks NVDA / JAWS. This undeoes ngAria's attrs.
36+
MenuItemController.prototype.clearNgAria = function() {
37+
var el = this.$element[0];
38+
var clearAttrs = ['role', 'tabindex', 'aria-invalid', 'aria-checked'];
39+
angular.forEach(clearAttrs, function(attr) {
40+
el.removeAttribute(attr);
41+
});
42+
};
43+
2944
MenuItemController.prototype.initClickListeners = function() {
45+
var self = this;
3046
var ngModel = this.ngModel;
3147
var $scope = this.$scope;
3248
var $attrs = this.$attrs;
@@ -35,20 +51,21 @@ MenuItemController.prototype.initClickListeners = function() {
3551

3652
this.handleClick = angular.bind(this, this.handleClick);
3753

38-
var icon = this.iconEl
54+
var icon = this.iconEl;
3955
var button = angular.element(this.buttonEl);
4056
var handleClick = this.handleClick;
4157

4258
$attrs.$observe('disabled', setDisabled);
4359
setDisabled($attrs.disabled);
4460

4561
ngModel.$render = function render() {
62+
self.clearNgAria();
4663
if (isSelected()) {
4764
icon.style.display = '';
48-
$element.attr('aria-checked', 'true');
65+
button.attr('aria-checked', 'true');
4966
} else {
5067
icon.style.display = 'none';
51-
$element.attr('aria-checked', 'false');
68+
button.attr('aria-checked', 'false');
5269
}
5370
};
5471

src/components/menuBar/js/menuItemDirective.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ angular
1010
function MenuItemDirective() {
1111
return {
1212
require: ['mdMenuItem', '?ngModel'],
13+
priority: 210, // ensure that our post link runs after ngAria
1314
compile: function(templateEl, templateAttrs) {
1415
if (templateAttrs.type == 'checkbox' || templateAttrs.type == 'radio') {
1516
var text = templateEl[0].textContent;

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,12 +228,13 @@ describe('material.components.menuBar', function() {
228228
});
229229
it('reflects the ng-model value', inject(function($rootScope) {
230230
var menuItem = setup('ng-model="test"')[0];
231-
expect(menuItem.getAttribute('aria-checked')).toBe('false');
231+
var button = menuItem.querySelector('md-button');
232+
expect(button.getAttribute('aria-checked')).toBe('false');
232233
expect(menuItem.children[0].style.display).toBe('none');
233234
$rootScope.test = true;
234235
$rootScope.$digest();
235236
expect(menuItem.children[0].style.display).toBe('');
236-
expect(menuItem.getAttribute('aria-checked')).toBe('true');
237+
expect(button.getAttribute('aria-checked')).toBe('true');
237238
}));
238239

239240
function setup(attrs) {
@@ -283,12 +284,13 @@ describe('material.components.menuBar', function() {
283284
it('reflects the ng-model value', inject(function($rootScope) {
284285
$rootScope.test = 'apple';
285286
var menuItem = setup('ng-model="test" value="hello"')[0];
286-
expect(menuItem.getAttribute('aria-checked')).toBe('false');
287+
var button = menuItem.querySelector('md-button');
288+
expect(button.getAttribute('aria-checked')).toBe('false');
287289
expect(menuItem.children[0].style.display).toBe('none');
288290
$rootScope.test = 'hello';
289291
$rootScope.$digest();
290292
expect(menuItem.children[0].style.display).toBeFalsy();
291-
expect(menuItem.getAttribute('aria-checked')).toBe('true');
293+
expect(button.getAttribute('aria-checked')).toBe('true');
292294
}));
293295

294296
function setup(attrs) {

0 commit comments

Comments
 (0)