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

Commit d9ba0e1

Browse files
rschmuklerThomasBurleson
authored andcommitted
feat(menuBar): add menu bar component
closes #78
1 parent c9f2b9f commit d9ba0e1

File tree

17 files changed

+1609
-212
lines changed

17 files changed

+1609
-212
lines changed

docs/app/img/icons/sets/core-icons.svg

Lines changed: 6 additions & 0 deletions
Loading

src/components/backdrop/backdrop.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
md-backdrop {
22
z-index: $z-index-backdrop;
33
&.md-menu-backdrop {
4+
position: fixed !important;
45
z-index: $z-index-menu - 1;
56
}
67
&.md-select-backdrop {

src/components/button/button.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ function MdButtonDirective($mdButtonInkRipple, $mdTheming, $mdAria, $timeout) {
116116
}, 100);
117117
})
118118
.on('focus', function() {
119-
if(scope.mouseActive === false) { element.addClass('md-focused'); }
119+
if (scope.mouseActive === false) { element.addClass('md-focused'); }
120120
})
121121
.on('blur', function() { element.removeClass('md-focused'); });
122122
}

src/components/menu/_menu.js

Lines changed: 144 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,11 @@ angular.module('material.components.menu', [
135135
*
136136
*/
137137

138-
function MenuDirective() {
138+
function MenuDirective($mdMenu, $mdUtil, $timeout) {
139139
var INVALID_PREFIX = 'Invalid HTML for md-menu: ';
140140
return {
141141
restrict: 'E',
142-
require: 'mdMenu',
142+
require: ['mdMenu', '?mdMenuBar'],
143143
controller: 'mdMenuCtrl', // empty function to be built by link
144144
scope: true,
145145
compile: compile
@@ -149,25 +149,51 @@ function MenuDirective() {
149149
templateElement.addClass('md-menu');
150150
var triggerElement = templateElement.children()[0];
151151
if (!triggerElement.hasAttribute('ng-click')) {
152-
triggerElement = triggerElement.querySelector('[ng-click],[ng-mouseenter]');
152+
triggerElement = triggerElement.querySelector('[ng-click],[ng-mouseenter]') || triggerElement;
153153
}
154+
if (triggerElement && (
155+
triggerElement.nodeName == 'MD-BUTTON' ||
156+
triggerElement.nodeName == 'BUTTON'
157+
) && !triggerElement.hasAttribute('type')) {
158+
triggerElement.setAttribute('type', 'button');
159+
}
160+
154161
if (templateElement.children().length != 2) {
155162
throw Error(INVALID_PREFIX + 'Expected two children elements.');
156163
}
157164

158165
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
159166
triggerElement && triggerElement.setAttribute('aria-haspopup', 'true');
167+
168+
var nestedMenus = templateElement[0].querySelectorAll('md-menu');
169+
var nestingDepth = parseInt(templateElement[0].getAttribute('md-nest-level'), 10) || 0;
170+
if (nestedMenus) {
171+
angular.forEach($mdUtil.nodesToArray(nestedMenus), function(menuEl) {
172+
if (!menuEl.hasAttribute('md-position-mode')) {
173+
menuEl.setAttribute('md-position-mode', 'cascade');
174+
}
175+
menuEl.classList.add('md-nested-menu');
176+
menuEl.setAttribute('md-nest-level', nestingDepth + 1);
177+
menuEl.setAttribute('role', 'menu');
178+
});
179+
}
160180
return link;
161181
}
162182

163-
function link(scope, element, attrs, mdMenuCtrl) {
183+
function link(scope, element, attrs, ctrls) {
184+
var mdMenuCtrl = ctrls[0];
185+
var isInMenuBar = ctrls[1] != undefined;
164186
// Move everything into a md-menu-container and pass it to the controller
165187
var menuContainer = angular.element(
166188
'<div class="md-open-menu-container md-whiteframe-z2"></div>'
167189
);
168190
var menuContents = element.children()[1];
169191
menuContainer.append(menuContents);
170-
mdMenuCtrl.init(menuContainer);
192+
if (isInMenuBar) {
193+
element.append(menuContainer);
194+
menuContainer[0].style.display = 'none';
195+
}
196+
mdMenuCtrl.init(menuContainer, { isInMenuBar: isInMenuBar });
171197

172198
scope.$on('$destroy', function() {
173199
menuContainer.remove();
@@ -177,69 +203,151 @@ function MenuDirective() {
177203
}
178204
}
179205

180-
function MenuController($mdMenu, $attrs, $element, $scope) {
206+
function MenuController($mdMenu, $attrs, $element, $scope, $mdUtil, $timeout) {
207+
181208
var menuContainer;
182-
var ctrl = this;
209+
var self = this;
183210
var triggerElement;
184211

185-
this.init = angular.bind(this, init);
186-
this.open = angular.bind(this, openMenu);
187-
this.close = angular.bind(this, closeMenu);
188-
189-
this.positionMode = angular.bind(this, positionMode);
190-
this.offsets = angular.bind(this, offsets);
191-
192-
// Expose a open function to the child scope for html to use
193-
$scope.$mdOpenMenu = this.open;
212+
this.nestLevel = parseInt($attrs.mdNestLevel, 10) || 0;
194213

195214
/**
196215
* Called by our linking fn to provide access to the menu-content
197216
* element removed during link
198217
*/
199-
function init(setMenuContainer) {
218+
this.init = function init(setMenuContainer, opts) {
219+
opts = opts || {};
200220
menuContainer = setMenuContainer;
201221
// Default element for ARIA attributes has the ngClick or ngMouseenter expression
202222
triggerElement = $element[0].querySelector('[ng-click],[ng-mouseenter]');
203-
}
223+
224+
this.isInMenuBar = opts.isInMenuBar;
225+
this.nestedMenus = $mdUtil.nodesToArray(menuContainer[0].querySelectorAll('.md-nested-menu'));
226+
this.enableHoverListener();
227+
228+
menuContainer.on('$mdInterimElementRemove', function() {
229+
self.isOpen = false;
230+
});
231+
};
232+
233+
this.enableHoverListener = function() {
234+
$scope.$on('$mdMenuOpen', function(event, el) {
235+
if (menuContainer[0].contains(el[0])) {
236+
self.currentlyOpenMenu = el.controller('mdMenu');
237+
self.isAlreadyOpening = false;
238+
self.currentlyOpenMenu.registerContainerProxy(self.triggerContainerProxy.bind(self));
239+
}
240+
});
241+
$scope.$on('$mdMenuClose', function(event, el) {
242+
if (menuContainer[0].contains(el[0])) {
243+
self.currentlyOpenMenu = undefined;
244+
}
245+
});
246+
247+
var menuItems = angular.element($mdUtil.nodesToArray(menuContainer[0].querySelectorAll('md-menu-item')));
248+
249+
var openMenuTimeout;
250+
menuItems.on('mouseenter', function(event) {
251+
if (self.isAlreadyOpening) return;
252+
var nestedMenu = (
253+
event.target.querySelector('md-menu')
254+
|| $mdUtil.getClosest(event.target, 'MD-MENU')
255+
);
256+
openMenuTimeout = $timeout(function() {
257+
if (nestedMenu) {
258+
nestedMenu = angular.element(nestedMenu).controller('mdMenu');
259+
}
260+
261+
if (self.currentlyOpenMenu && self.currentlyOpenMenu != nestedMenu) {
262+
var closeTo = self.nestLevel + 1;
263+
self.currentlyOpenMenu.close(true, { closeTo: closeTo });
264+
} else if (nestedMenu && !nestedMenu.isOpen && nestedMenu.open) {
265+
self.isAlreadyOpening = true;
266+
nestedMenu.open();
267+
}
268+
}, nestedMenu ? 100 : 250);
269+
var focusableTarget = event.currentTarget.querySelector('[tabindex]');
270+
focusableTarget && focusableTarget.focus();
271+
});
272+
menuItems.on('mouseleave', function(event) {
273+
if (openMenuTimeout) {
274+
$timeout.cancel(openMenuTimeout);
275+
openMenuTimeout = undefined;
276+
}
277+
});
278+
};
204279

205280
/**
206281
* Uses the $mdMenu interim element service to open the menu contents
207282
*/
208-
function openMenu(ev) {
283+
this.open = function openMenu(ev) {
209284
ev && ev.stopPropagation();
210-
285+
ev && ev.preventDefault();
286+
if (self.isOpen) return;
287+
self.isOpen = true;
211288
triggerElement = triggerElement || (ev ? ev.target : $element[0]);
212-
triggerElement.setAttribute('aria-expanded', 'true');
213-
214-
ctrl.isOpen = true;
289+
$scope.$emit('$mdMenuOpen', $element);
215290
$mdMenu.show({
216291
scope: $scope,
217-
mdMenuCtrl: ctrl,
292+
mdMenuCtrl: self,
293+
nestLevel: self.nestLevel,
218294
element: menuContainer,
219-
target: triggerElement
295+
target: triggerElement,
296+
preserveElement: self.isInMenuBar || self.nestedMenus.length > 0,
297+
parent: self.isInMenuBar ? $element : 'body'
220298
});
221299
}
222300

223-
/**
224-
* Use the $mdMenu interim element service to close the menu contents
225-
*/
226-
function closeMenu(skipFocus) {
227-
if ( !ctrl.isOpen ) return;
301+
// Expose a open function to the child scope for html to use
302+
$scope.$mdOpenMenu = this.open;
303+
304+
$scope.$watch(function() { return self.isOpen; }, function(isOpen) {
305+
if (isOpen) {
306+
triggerElement.setAttribute('aria-expanded', 'true');
307+
$element[0].classList.add('md-open');
308+
angular.forEach(self.nestedMenus, function(el) {
309+
el.classList.remove('md-open');
310+
});
311+
} else {
312+
triggerElement && triggerElement.setAttribute('aria-expanded', 'false');
313+
$element[0].classList.remove('md-open');
314+
}
315+
$scope.$mdMenuIsOpen = self.isOpen;
316+
});
317+
318+
this.focusMenuContainer = function focusMenuContainer() {
319+
var focusTarget = menuContainer[0].querySelector('[md-menu-focus-target]');
320+
if (!focusTarget) focusTarget = menuContainer[0].querySelector('.md-button');
321+
focusTarget.focus();
322+
};
323+
324+
this.registerContainerProxy = function registerContainerProxy(handler) {
325+
this.containerProxy = handler;
326+
};
327+
328+
this.triggerContainerProxy = function triggerContainerProxy(ev) {
329+
this.containerProxy && this.containerProxy(ev);
330+
};
228331

229-
ctrl.isOpen = false;
230-
triggerElement && triggerElement.setAttribute('aria-expanded', 'false');
231-
$mdMenu.hide();
332+
// Use the $mdMenu interim element service to close the menu contents
333+
this.close = function closeMenu(skipFocus, closeOpts) {
334+
if ( !self.isOpen ) return;
335+
self.isOpen = false;
232336

337+
$scope.$emit('$mdMenuClose', $element);
338+
$mdMenu.hide(null, closeOpts);
233339
if (!skipFocus) {
234-
$element.children()[0].focus();
340+
var el = self.restoreFocusTo || $element.find('button')[0];
341+
if (el instanceof angular.element) el = el[0];
342+
el.focus();
235343
}
236344
}
237345

238346
/**
239347
* Build a nice object out of our string attribute which specifies the
240348
* target mode for left and top positioning
241349
*/
242-
function positionMode() {
350+
this.positionMode = function positionMode() {
243351
var attachment = ($attrs.mdPositionMode || 'target').split(' ');
244352

245353
// If attachment is a single item, duplicate it for our second value.
@@ -258,7 +366,7 @@ function MenuController($mdMenu, $attrs, $element, $scope) {
258366
* Build a nice object out of our string attribute which specifies
259367
* the offset of top and left in pixels.
260368
*/
261-
function offsets() {
369+
this.offsets = function offsets() {
262370
var offsets = ($attrs.mdOffset || '0 0').split(' ').map(parseFloat);
263371
if (offsets.length == 2) {
264372
return {

0 commit comments

Comments
 (0)