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

Commit

Permalink
update(menu): support option ng-mouseenter to open menu
Browse files Browse the repository at this point in the history
allow ng-mouseenter as an option to open a menu instead of just ng-click.
allow triggerElement to be determined when $event is undefined.

Closes #4020.
  • Loading branch information
GlennButera authored and ThomasBurleson committed Aug 8, 2015
1 parent 1daa8bc commit 4111c93
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 37 deletions.
79 changes: 51 additions & 28 deletions src/components/menu/_menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ angular.module('material.components.menu', [
*/

function MenuDirective($mdMenu) {
var INVALID_PREFIX = 'Invalid HTML for md-menu: ';
return {
restrict: 'E',
require: 'mdMenu',
Expand All @@ -148,12 +149,14 @@ function MenuDirective($mdMenu) {
templateElement.addClass('md-menu');
var triggerElement = templateElement.children()[0];
if (!triggerElement.hasAttribute('ng-click')) {
triggerElement = triggerElement.querySelector('[ng-click]');
triggerElement = triggerElement.querySelector('[ng-click],[ng-mouseenter]');
}
triggerElement && triggerElement.setAttribute('aria-haspopup', 'true');
if (templateElement.children().length != 2) {
throw Error('Invalid HTML for md-menu. Expected two children elements.');
throw Error(INVALID_PREFIX + 'Expected two children elements.');
}

// Default element for ARIA attributes has the ngClick or ngMouseenter expression
triggerElement && triggerElement.setAttribute('aria-haspopup', 'true');
return link;
}

Expand All @@ -175,50 +178,68 @@ function MenuDirective($mdMenu) {
}

function MenuController($mdMenu, $attrs, $element, $scope) {

var menuContainer;
var ctrl = this;
var triggerElement;

// Called by our linking fn to provide access to the menu-content
// element removed during link
this.init = function(setMenuContainer) {
this.init = angular.bind(this, init);
this.open = angular.bind(this, openMenu);
this.close = angular.bind(this, closeMenu);

this.positionMode = angular.bind(this, positionMode);
this.offsets = angular.bind(this, offsets);

// Expose a open function to the child scope for html to use
$scope.$mdOpenMenu = this.open;

/**
* Called by our linking fn to provide access to the menu-content
* element removed during link
*/
function init(setMenuContainer) {
menuContainer = setMenuContainer;
triggerElement = $element[0].querySelector('[ng-click]');
};
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
triggerElement = $element[0].querySelector('[ng-click],[ng-mouseenter]');
}

// Uses the $mdMenu interim element service to open the menu contents
this.open = function openMenu(ev) {
/**
* Uses the $mdMenu interim element service to open the menu contents
*/
function openMenu(ev) {
ev && ev.stopPropagation();

ctrl.isOpen = true;
triggerElement = triggerElement || (ev ? ev.target : $element[0]);
triggerElement.setAttribute('aria-expanded', 'true');

ctrl.isOpen = true;
$mdMenu.show({
scope: $scope,
mdMenuCtrl: ctrl,
element: menuContainer,
target: $element[0]
target: triggerElement
});
};
// Expose a open function to the child scope for html to use
$scope.$mdOpenMenu = this.open;
}

// Use the $mdMenu interim element service to close the menu contents
this.close = function closeMenu(skipFocus) {
/**
* Use the $mdMenu interim element service to close the menu contents
*/
function closeMenu(skipFocus) {
if ( !ctrl.isOpen ) return;

ctrl.isOpen = false;
triggerElement.setAttribute('aria-expanded', 'false');
triggerElement && triggerElement.setAttribute('aria-expanded', 'false');
$mdMenu.hide();

if (!skipFocus) {
$element.children()[0].focus();
}
};
}

// Build a nice object out of our string attribute which specifies the
// target mode for left and top positioning
this.positionMode = function() {
/**
* Build a nice object out of our string attribute which specifies the
* target mode for left and top positioning
*/
function positionMode() {
var attachment = ($attrs.mdPositionMode || 'target').split(' ');

// If attachment is a single item, duplicate it for our second value.
Expand All @@ -231,11 +252,13 @@ function MenuController($mdMenu, $attrs, $element, $scope) {
left: attachment[0],
top: attachment[1]
};
};
}

// Build a nice object out of our string attribute which specifies
// the offset of top and left in pixels.
this.offsets = function() {
/**
* Build a nice object out of our string attribute which specifies
* the offset of top and left in pixels.
*/
function offsets() {
var offsets = ($attrs.mdOffset || '0 0').split(' ').map(parseFloat);
if (offsets.length == 2) {
return {
Expand All @@ -250,5 +273,5 @@ function MenuController($mdMenu, $attrs, $element, $scope) {
} else {
throw Error('Invalid offsets specified. Please follow format <x, y> or <n>');
}
};
}
}
2 changes: 1 addition & 1 deletion src/components/menu/demoBasicUsage/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ <h2 class="md-title">Simple dropdown menu</h2>
<p>Applying the <code>md-menu-origin</code> and <code>md-menu-align-target</code> attributes ensure that the menu elements align.
Note: If you select the Redial menu option, then a modal dialog will zoom out of the phone icon button.</p>
<md-menu>
<md-button aria-label="Open phone interactions menu" class="md-icon-button" ng-click="ctrl.openMenu($mdOpenMenu, $event)">
<md-button aria-label="Open phone interactions menu" class="md-icon-button" ng-mouseenter="ctrl.openMenu($mdOpenMenu, $event)">
<md-icon md-menu-origin md-svg-icon="call:phone"></md-icon>
</md-button>
<md-menu-content width="4">
Expand Down
43 changes: 35 additions & 8 deletions src/components/menu/menu.spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
describe('md-menu directive', function() {
var $mdMenu, $timeout, something;
var $mdMenu, $timeout, something, $mdUtil;

beforeEach(module('material.components.menu'));
beforeEach(inject(function($mdUtil, $$q, $document, _$mdMenu_, _$timeout_) {
beforeEach(inject(function(_$mdUtil_, _$mdMenu_, _$timeout_, $document) {
$mdUtil = _$mdUtil_;
$mdMenu = _$mdMenu_;
$timeout = _$timeout_;
var abandonedMenus = $document[0].querySelectorAll('.md-menu-container');
Expand Down Expand Up @@ -34,6 +35,32 @@ describe('md-menu directive', function() {
expect(getOpenMenuContainer().length).toBe(0);
});

it('opens on click without $event', function() {
var noEvent = true;
var menu = setup('ng-click', noEvent);
openMenu(menu);
expect(getOpenMenuContainer().length).toBe(1);
closeMenu(menu);
expect(getOpenMenuContainer().length).toBe(0);
});

it('opens on mouseEnter', function() {
var menu = setup('ng-mouseenter');
openMenu(menu, 'mouseenter');
expect(getOpenMenuContainer().length).toBe(1);
closeMenu(menu);
expect(getOpenMenuContainer().length).toBe(0);
});

it('opens on mouseEnter without $event', function() {
var noEvent = true;
var menu = setup('ng-mouseenter', noEvent);
openMenu(menu, 'mouseenter');
expect(getOpenMenuContainer().length).toBe(1);
closeMenu(menu);
expect(getOpenMenuContainer().length).toBe(0);
});

it('should not propagate the click event', function() {
var clickDetected = false, menu = setup();
menu.on('click', function() {
Expand Down Expand Up @@ -129,17 +156,17 @@ describe('md-menu directive', function() {
// Internal methods
// ********************************************

function setup() {
function setup(triggerType, noEvent) {
var menu,
template = '' +
template = $mdUtil.supplant('' +
'<md-menu>' +
' <button ng-click="$mdOpenMenu($event)">Hello World</button>' +
' <button {0}="$mdOpenMenu({1})">Hello World</button>' +
' <md-menu-content>' +
' <md-menu-item>' +
' <md-button ng-click="doSomething($event)"></md-button>' +
' </md-menu-item>' +
' </md-menu-content>' +
'</md-menu>';
'</md-menu>',[ triggerType || 'ng-click', noEvent ? '' : "$event" ]);

inject(function($compile, $rootScope) {
$rootScope.doSomething = function($event) {
Expand All @@ -159,8 +186,8 @@ describe('md-menu directive', function() {
return res;
}

function openMenu(el) {
el.children().eq(0).triggerHandler('click');
function openMenu(el, triggerType) {
el.children().eq(0).triggerHandler(triggerType || 'click');
waitForMenuOpen();
}

Expand Down

0 comments on commit 4111c93

Please sign in to comment.