2 * Angular Material Design
3 * https://github.com/angular/material
7 goog.provide('ngmaterial.components.navBar');
8 goog.require('ngmaterial.core');
11 * @name material.components.navBar
15 MdNavBarController['$inject'] = ["$element", "$scope", "$timeout", "$mdConstant"];
16 MdNavItem['$inject'] = ["$mdAria", "$$rAF"];
17 MdNavItemController['$inject'] = ["$element"];
18 MdNavBar['$inject'] = ["$mdAria", "$mdTheming"];
19 angular.module('material.components.navBar', ['material.core'])
20 .controller('MdNavBarController', MdNavBarController)
21 .directive('mdNavBar', MdNavBar)
22 .controller('MdNavItemController', MdNavItemController)
23 .directive('mdNavItem', MdNavItem);
26 /*****************************************************************************
27 * PUBLIC DOCUMENTATION *
28 *****************************************************************************/
32 * @module material.components.navBar
37 * The `<md-nav-bar>` directive renders a list of material tabs that can be used
38 * for top-level page navigation. Unlike `<md-tabs>`, it has no concept of a tab
39 * body and no bar pagination.
41 * Because it deals with page navigation, certain routing concepts are built-in.
42 * Route changes via via ng-href, ui-sref, or ng-click events are supported.
43 * Alternatively, the user could simply watch currentNavItem for changes.
45 * Accessibility functionality is implemented as a site navigator with a
46 * listbox, according to
47 * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
49 * @param {string=} mdSelectedNavItem The name of the current tab; this must
50 * match the name attribute of `<md-nav-item>`
51 * @param {boolean=} mdNoInkBar If set to true, the ink bar will be hidden.
52 * @param {string=} navBarAriaLabel An aria-label for the nav-bar
56 * <md-nav-bar md-selected-nav-item="currentNavItem">
57 * <md-nav-item md-nav-click="goto('page1')" name="page1">
60 * <md-nav-item md-nav-href="#page2" name="page3">Page Two</md-nav-item>
61 * <md-nav-item md-nav-sref="page3" name="page2">Page Three</md-nav-item>
63 * md-nav-sref="app.page4"
64 * sref-opts="{reload: true, notify: true}"
74 * $rootScope.$on('$routeChangeSuccess', function(event, current) {
75 * $scope.currentLink = getCurrentLinkFromRoute(current);
81 /*****************************************************************************
83 *****************************************************************************/
87 * @module material.components.navBar
92 * `<md-nav-item>` describes a page navigation link within the `<md-nav-bar>`
93 * component. It renders an md-button as the actual link.
95 * Exactly one of the mdNavClick, mdNavHref, mdNavSref attributes are required
98 * @param {Function=} mdNavClick Function which will be called when the
99 * link is clicked to change the page. Renders as an `ng-click`.
100 * @param {string=} mdNavHref url to transition to when this link is clicked.
101 * Renders as an `ng-href`.
102 * @param {string=} mdNavSref Ui-router state to transition to when this link is
103 * clicked. Renders as a `ui-sref`.
104 * @param {!Object=} srefOpts Ui-router options that are passed to the
105 * `$state.go()` function. See the [Ui-router documentation for details]
106 * (https://ui-router.github.io/docs/latest/interfaces/transition.transitionoptions.html).
107 * @param {string=} name The name of this link. Used by the nav bar to know
108 * which link is currently selected.
109 * @param {string=} aria-label Adds alternative text for accessibility
112 * See `<md-nav-bar>` for usage.
116 /*****************************************************************************
118 *****************************************************************************/
120 function MdNavBar($mdAria, $mdTheming) {
124 controller: MdNavBarController,
125 controllerAs: 'ctrl',
126 bindToController: true,
128 'mdSelectedNavItem': '=?',
130 'navBarAriaLabel': '@?',
133 '<div class="md-nav-bar">' +
134 '<nav role="navigation">' +
135 '<ul class="_md-nav-bar-list" ng-transclude role="listbox"' +
137 'ng-focus="ctrl.onFocus()"' +
138 'ng-keydown="ctrl.onKeydown($event)"' +
139 'aria-label="{{ctrl.navBarAriaLabel}}">' +
142 '<md-nav-ink-bar ng-hide="ctrl.mdNoInkBar"></md-nav-ink-bar>' +
144 link: function(scope, element, attrs, ctrl) {
146 if (!ctrl.navBarAriaLabel) {
147 $mdAria.expectAsync(element, 'aria-label', angular.noop);
154 * Controller for the nav-bar component.
156 * Accessibility functionality is implemented as a site navigator with a
157 * listbox, according to
158 * https://www.w3.org/TR/wai-aria-practices/#Site_Navigator_Tabbed_Style
159 * @param {!angular.JQLite} $element
160 * @param {!angular.Scope} $scope
161 * @param {!angular.Timeout} $timeout
162 * @param {!Object} $mdConstant
167 function MdNavBarController($element, $scope, $timeout, $mdConstant) {
168 // Injected variables
169 /** @private @const {!angular.Timeout} */
170 this._$timeout = $timeout;
172 /** @private @const {!angular.Scope} */
173 this._$scope = $scope;
175 /** @private @const {!Object} */
176 this._$mdConstant = $mdConstant;
178 // Data-bound variables.
179 /** @type {string} */
180 this.mdSelectedNavItem;
182 /** @type {string} */
183 this.navBarAriaLabel;
187 /** @type {?angular.JQLite} */
188 this._navBarEl = $element[0];
190 /** @type {?angular.JQLite} */
194 // need to wait for transcluded content to be available
195 var deregisterTabWatch = this._$scope.$watch(function() {
196 return self._navBarEl.querySelectorAll('._md-nav-button').length;
198 function(newLength) {
201 deregisterTabWatch();
209 * Initializes the tab components once they exist.
212 MdNavBarController.prototype._initTabs = function() {
213 this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar'));
216 this._$timeout(function() {
217 self._updateTabs(self.mdSelectedNavItem, undefined);
220 this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) {
221 // Wait a digest before update tabs for products doing
222 // anything dynamic in the template.
223 self._$timeout(function() {
224 self._updateTabs(newValue, oldValue);
230 * Set the current tab to be selected.
231 * @param {string|undefined} newValue New current tab name.
232 * @param {string|undefined} oldValue Previous tab name.
235 MdNavBarController.prototype._updateTabs = function(newValue, oldValue) {
237 var tabs = this._getTabs();
239 // this._getTabs can return null if nav-bar has not yet been initialized
245 var newTab = this._getTabByName(newValue);
246 var oldTab = this._getTabByName(oldValue);
249 oldTab.setSelected(false);
250 oldIndex = tabs.indexOf(oldTab);
254 newTab.setSelected(true);
255 newIndex = tabs.indexOf(newTab);
258 this._$timeout(function() {
259 self._updateInkBarStyles(newTab, newIndex, oldIndex);
264 * Repositions the ink bar to the selected tab.
267 MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex, oldIndex) {
268 this._inkbar.toggleClass('_md-left', newIndex < oldIndex)
269 .toggleClass('_md-right', newIndex > oldIndex);
271 this._inkbar.css({display: newIndex < 0 ? 'none' : ''});
274 var tabEl = tab.getButtonEl();
275 var left = tabEl.offsetLeft;
277 this._inkbar.css({left: left + 'px', width: tabEl.offsetWidth + 'px'});
282 * Returns an array of the current tabs.
283 * @return {!Array<!NavItemController>}
286 MdNavBarController.prototype._getTabs = function() {
287 var controllers = Array.prototype.slice.call(
288 this._navBarEl.querySelectorAll('.md-nav-item'))
290 return angular.element(el).controller('mdNavItem')
292 return controllers.indexOf(undefined) ? controllers : null;
296 * Returns the tab with the specified name.
297 * @param {string} name The name of the tab, found in its name attribute.
298 * @return {!NavItemController|undefined}
301 MdNavBarController.prototype._getTabByName = function(name) {
302 return this._findTab(function(tab) {
303 return tab.getName() == name;
308 * Returns the selected tab.
309 * @return {!NavItemController|undefined}
312 MdNavBarController.prototype._getSelectedTab = function() {
313 return this._findTab(function(tab) {
314 return tab.isSelected();
319 * Returns the focused tab.
320 * @return {!NavItemController|undefined}
322 MdNavBarController.prototype.getFocusedTab = function() {
323 return this._findTab(function(tab) {
324 return tab.hasFocus();
329 * Find a tab that matches the specified function.
332 MdNavBarController.prototype._findTab = function(fn) {
333 var tabs = this._getTabs();
334 for (var i = 0; i < tabs.length; i++) {
344 * Direct focus to the selected tab when focus enters the nav bar.
346 MdNavBarController.prototype.onFocus = function() {
347 var tab = this._getSelectedTab();
349 tab.setFocused(true);
354 * Move focus from oldTab to newTab.
355 * @param {!NavItemController} oldTab
356 * @param {!NavItemController} newTab
359 MdNavBarController.prototype._moveFocus = function(oldTab, newTab) {
360 oldTab.setFocused(false);
361 newTab.setFocused(true);
365 * Responds to keypress events.
368 MdNavBarController.prototype.onKeydown = function(e) {
369 var keyCodes = this._$mdConstant.KEY_CODE;
370 var tabs = this._getTabs();
371 var focusedTab = this.getFocusedTab();
372 if (!focusedTab) return;
374 var focusedTabIndex = tabs.indexOf(focusedTab);
376 // use arrow keys to navigate between tabs
378 case keyCodes.UP_ARROW:
379 case keyCodes.LEFT_ARROW:
380 if (focusedTabIndex > 0) {
381 this._moveFocus(focusedTab, tabs[focusedTabIndex - 1]);
384 case keyCodes.DOWN_ARROW:
385 case keyCodes.RIGHT_ARROW:
386 if (focusedTabIndex < tabs.length - 1) {
387 this._moveFocus(focusedTab, tabs[focusedTabIndex + 1]);
392 // timeout to avoid a "digest already in progress" console error
393 this._$timeout(function() {
394 focusedTab.getButtonEl().click();
403 function MdNavItem($mdAria, $$rAF) {
406 require: ['mdNavItem', '^mdNavBar'],
407 controller: MdNavItemController,
408 bindToController: true,
409 controllerAs: 'ctrl',
412 template: function(tElement, tAttrs) {
413 var hasNavClick = tAttrs.mdNavClick;
414 var hasNavHref = tAttrs.mdNavHref;
415 var hasNavSref = tAttrs.mdNavSref;
416 var hasSrefOpts = tAttrs.srefOpts;
417 var navigationAttribute;
418 var navigationOptions;
421 // Cannot specify more than one nav attribute
422 if ((hasNavClick ? 1:0) + (hasNavHref ? 1:0) + (hasNavSref ? 1:0) > 1) {
424 'Must not specify more than one of the md-nav-click, md-nav-href, ' +
425 'or md-nav-sref attributes per nav-item directive.'
430 navigationAttribute = 'ng-click="ctrl.mdNavClick()"';
431 } else if (hasNavHref) {
432 navigationAttribute = 'ng-href="{{ctrl.mdNavHref}}"';
433 } else if (hasNavSref) {
434 navigationAttribute = 'ui-sref="{{ctrl.mdNavSref}}"';
437 navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : '';
439 if (navigationAttribute) {
440 buttonTemplate = '' +
441 '<md-button class="_md-nav-button md-accent" ' +
442 'ng-class="ctrl.getNgClassMap()" ' +
443 'ng-blur="ctrl.setFocused(false)" ' +
446 navigationAttribute + '>' +
447 '<span ng-transclude class="_md-nav-button-text"></span>' +
452 '<li class="md-nav-item" ' +
454 'aria-selected="{{ctrl.isSelected()}}">' +
455 (buttonTemplate || '') +
465 link: function(scope, element, attrs, controllers) {
466 // When accessing the element's contents synchronously, they
467 // may not be defined yet because of transclusion. There is a higher
468 // chance that it will be accessible if we wait one frame.
470 var mdNavItem = controllers[0];
471 var mdNavBar = controllers[1];
472 var navButton = angular.element(element[0].querySelector('._md-nav-button'));
474 if (!mdNavItem.name) {
475 mdNavItem.name = angular.element(element[0]
476 .querySelector('._md-nav-button-text')).text().trim();
479 navButton.on('click', function() {
480 mdNavBar.mdSelectedNavItem = mdNavItem.name;
484 $mdAria.expectWithText(element, 'aria-label');
491 * Controller for the nav-item component.
492 * @param {!angular.JQLite} $element
497 function MdNavItemController($element) {
499 /** @private @const {!angular.JQLite} */
500 this._$element = $element;
502 // Data-bound variables
504 /** @const {?Function} */
507 /** @const {?string} */
510 /** @const {?string} */
512 /** @const {?Object} */
514 /** @const {?string} */
518 /** @private {boolean} */
519 this._selected = false;
521 /** @private {boolean} */
522 this._focused = false;
526 * Returns a map of class names and values for use by ng-class.
527 * @return {!Object<string,boolean>}
529 MdNavItemController.prototype.getNgClassMap = function() {
531 'md-active': this._selected,
532 'md-primary': this._selected,
533 'md-unselected': !this._selected,
534 'md-focused': this._focused,
539 * Get the name attribute of the tab.
542 MdNavItemController.prototype.getName = function() {
547 * Get the button element associated with the tab.
550 MdNavItemController.prototype.getButtonEl = function() {
551 return this._$element[0].querySelector('._md-nav-button');
555 * Set the selected state of the tab.
556 * @param {boolean} isSelected
558 MdNavItemController.prototype.setSelected = function(isSelected) {
559 this._selected = isSelected;
565 MdNavItemController.prototype.isSelected = function() {
566 return this._selected;
570 * Set the focused state of the tab.
571 * @param {boolean} isFocused
573 MdNavItemController.prototype.setFocused = function(isFocused) {
574 this._focused = isFocused;
577 this.getButtonEl().focus();
584 MdNavItemController.prototype.hasFocus = function() {
585 return this._focused;
588 ngmaterial.components.navBar = angular.module("material.components.navBar");