diff --git a/src/components/menu/js/menuController.js b/src/components/menu/js/menuController.js index 9099f541026..0bf5097233f 100644 --- a/src/components/menu/js/menuController.js +++ b/src/components/menu/js/menuController.js @@ -25,6 +25,7 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $r // Default element for ARIA attributes has the ngClick or ngMouseenter expression triggerElement = $element[0].querySelector('[ng-click],[ng-mouseenter]'); + this.isInMenuBar = opts.isInMenuBar; this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu')); menuContainer.on('$mdInterimElementRemove', function() { @@ -117,7 +118,7 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $r element: menuContainer, target: triggerElement, preserveElement: true, - parent: $element + parent: 'body' }).finally(function() { self.disableHoverListener(); }); @@ -128,14 +129,12 @@ function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout, $r $scope.$watch(function() { return self.isOpen; }, function(isOpen) { if (isOpen) { - triggerElement.setAttribute('aria-expanded', 'true'); menuContainer.attr('aria-hidden', 'false'); $element[0].classList.add('md-open'); angular.forEach(self.nestedMenus, function(el) { el.classList.remove('md-open'); }); } else { - triggerElement && triggerElement.setAttribute('aria-expanded', 'false'); menuContainer.attr('aria-hidden', 'true'); $element[0].classList.remove('md-open'); } diff --git a/src/components/menu/js/menuDirective.js b/src/components/menu/js/menuDirective.js index 5046411d315..3cbc738b720 100644 --- a/src/components/menu/js/menuDirective.js +++ b/src/components/menu/js/menuDirective.js @@ -204,6 +204,10 @@ function MenuDirective($mdUtil) { } menuContainer.append(menuContents); + element.on('$destroy', function() { + menuContainer.remove(); + }); + element.append(menuContainer); menuContainer[0].style.display = 'none'; mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar }); diff --git a/src/components/menu/js/menuServiceProvider.js b/src/components/menu/js/menuServiceProvider.js index 45b2e1c11fa..30d31710b94 100644 --- a/src/components/menu/js/menuServiceProvider.js +++ b/src/components/menu/js/menuServiceProvider.js @@ -127,11 +127,8 @@ function MenuProvider($$interimElementProvider) { * Place the menu into the DOM and call positioning related functions */ function showMenu() { - if (!opts.preserveElement) { - opts.parent.append(element); - } else { - element[0].style.display = ''; - } + opts.parent.append(element); + element[0].style.display = ''; return $q(function(resolve) { var position = calculateMenuPosition(element, opts); @@ -294,7 +291,7 @@ function MenuProvider($$interimElementProvider) { if ((hasAnyAttribute(target, ['ng-click', 'ng-href', 'ui-sref']) || target.nodeName == 'BUTTON' || target.nodeName == 'MD-BUTTON') && !hasAnyAttribute(target, ['md-prevent-menu-close'])) { var closestMenu = $mdUtil.getClosest(target, 'MD-MENU'); - if (!target.hasAttribute('disabled') && (closestMenu == opts.parent[0])) { + if (!target.hasAttribute('disabled') && (!closestMenu || closestMenu == opts.parent[0])) { close(); } break; diff --git a/src/components/menu/menu.spec.js b/src/components/menu/menu.spec.js index 85933daa6b9..bd73c3bf370 100644 --- a/src/components/menu/menu.spec.js +++ b/src/components/menu/menu.spec.js @@ -1,4 +1,4 @@ -describe('material.components.menu', function() { +ddescribe('material.components.menu', function() { var attachedElements = []; var $mdMenu, $timeout, menuActionPerformed, $mdUtil; @@ -7,7 +7,7 @@ describe('material.components.menu', function() { $mdUtil = _$mdUtil_; $mdMenu = _$mdMenu_; $timeout = _$timeout_; - var abandonedMenus = $document[0].querySelectorAll('.md-menu-container'); + var abandonedMenus = $document[0].querySelectorAll('.md-open-menu-container'); angular.element(abandonedMenus).remove(); })); afterEach(function() { @@ -41,6 +41,23 @@ describe('material.components.menu', function() { expect(getOpenMenuContainer(menu).length).toBe(0); }); + it('cleans up an open menu when the element leaves the DOM', function() { + var menu = setup(); + openMenu(menu); + menu.remove(); + expect(getOpenMenuContainer(menu).length).toBe(0); + }); + + it('sets up proper aria-owns and aria-haspopup relations between the button and the container', function() { + var menu = setup(); + var button = menu[0].querySelector('button'); + expect(button.hasAttribute('aria-haspopup')).toBe(true); + expect(button.hasAttribute('aria-owns')).toBe(true); + var ownsId = button.getAttribute('aria-owns'); + openMenu(menu); + expect(getOpenMenuContainer(menu).attr('id')).toBe(ownsId); + }); + it('opens on click without $event', function() { var noEvent = true; var menu = setup('ng-click', noEvent); @@ -98,7 +115,7 @@ describe('material.components.menu', function() { openMenu(menu); expect(getOpenMenuContainer(menu).length).toBe(1); - var openMenuEl = menu[0].querySelector('md-menu-content'); + var openMenuEl = $document[0].querySelector('md-menu-content'); pressKey(openMenuEl, $mdConstant.KEY_CODE.ESCAPE); waitForMenuClose(); @@ -197,21 +214,22 @@ describe('material.components.menu', function() { // ******************************************** function getOpenMenuContainer(el) { + var res; el = (el instanceof angular.element) ? el[0] : el; - var container = el.querySelector('.md-open-menu-container'); - if (container.style.display == 'none') { - return angular.element([]); - } else { - return angular.element(container); - } + inject(function($document) { + var container = $document[0].querySelector('.md-open-menu-container'); + if (container && container.style.display == 'none') { + res = []; + } else { + res = angular.element(container); + } + }); + return res; } function openMenu(el, triggerType) { - inject(function($document) { - el.children().eq(0).triggerHandler(triggerType || 'click'); - $document[0].body.appendChild(el[0]); - waitForMenuOpen(); - }); + el.children().eq(0).triggerHandler(triggerType || 'click'); + waitForMenuOpen(); } function closeMenu() {