1 | /*!
|
---|
2 | * AngularJS Material Design
|
---|
3 | * https://github.com/angular/material
|
---|
4 | * @license MIT
|
---|
5 | * v1.2.3
|
---|
6 | */
|
---|
7 | (function( window, angular, undefined ){
|
---|
8 | "use strict";
|
---|
9 |
|
---|
10 | /**
|
---|
11 | * @ngdoc module
|
---|
12 | * @name material.components.navBar
|
---|
13 | */
|
---|
14 | MdNavBar['$inject'] = ["$mdAria", "$mdTheming", "$window", "$mdUtil"];
|
---|
15 | MdNavBarController['$inject'] = ["$element", "$scope", "$timeout", "$mdConstant"];
|
---|
16 | MdNavItem['$inject'] = ["$mdAria", "$$rAF", "$mdUtil", "$window"];
|
---|
17 | MdNavItemController['$inject'] = ["$element"];
|
---|
18 | angular.module('material.components.navBar', ['material.core'])
|
---|
19 | .controller('MdNavBarController', MdNavBarController)
|
---|
20 | .directive('mdNavBar', MdNavBar)
|
---|
21 | .controller('MdNavItemController', MdNavItemController)
|
---|
22 | .directive('mdNavItem', MdNavItem);
|
---|
23 |
|
---|
24 | /**
|
---|
25 | * @ngdoc directive
|
---|
26 | * @name mdNavBar
|
---|
27 | * @module material.components.navBar
|
---|
28 | *
|
---|
29 | * @restrict E
|
---|
30 | *
|
---|
31 | * @description
|
---|
32 | * The `<md-nav-bar>` directive renders a list of material tabs that can be used
|
---|
33 | * for top-level page navigation. Unlike `<md-tabs>`, it has no concept of a tab
|
---|
34 | * body and no bar pagination.
|
---|
35 | *
|
---|
36 | * Because it deals with page navigation, certain routing concepts are built-in.
|
---|
37 | * Route changes via `ng-href`, `ui-sref`, or `ng-click` events are supported.
|
---|
38 | * Alternatively, the user could simply watch the value of `md-selected-nav-item`
|
---|
39 | * (`currentNavItem` in the below example) for changes.
|
---|
40 | *
|
---|
41 | * Accessibility functionality is implemented as a
|
---|
42 | * <a href="https://www.w3.org/TR/wai-aria-1.0/complete#tablist">
|
---|
43 | * tablist</a> with
|
---|
44 | * <a href="https://www.w3.org/TR/wai-aria-1.0/complete#tab">tabs</a>.
|
---|
45 | * We've kept the `role="navigation"` on the `<nav>`, for backwards compatibility, even though
|
---|
46 | * it is not required in the
|
---|
47 | * <a href="https://www.w3.org/TR/wai-aria-practices/#aria_lh_navigation">
|
---|
48 | * latest Working Group Note from December 2017</a>.
|
---|
49 | *
|
---|
50 | * <h3>Keyboard Navigation</h3>
|
---|
51 | * - `Tab`/`Shift+Tab` moves the focus to the next/previous interactive element on the page
|
---|
52 | * - `Enter`/`Space` selects the focused nav item and navigates to display the item's contents
|
---|
53 | * - `Right`/`Down` moves focus to the next nav item, wrapping at the end
|
---|
54 | * - `Left`/`Up` moves focus to the previous nav item, wrapping at the end
|
---|
55 | * - `Home`/`End` moves the focus to the first/last nav item
|
---|
56 | *
|
---|
57 | * @param {string=} md-selected-nav-item The name of the current tab; this must
|
---|
58 | * match the `name` attribute of `<md-nav-item>`.
|
---|
59 | * @param {boolean=} md-no-ink-bar If set to true, the ink bar will be hidden.
|
---|
60 | * @param {string=} nav-bar-aria-label An `aria-label` applied to the `md-nav-bar`'s tablist
|
---|
61 | * for accessibility.
|
---|
62 | *
|
---|
63 | * @usage
|
---|
64 | * <hljs lang="html">
|
---|
65 | * <md-nav-bar md-selected-nav-item="currentNavItem">
|
---|
66 | * <md-nav-item md-nav-click="goto('page1')" name="page1">
|
---|
67 | * Page One
|
---|
68 | * </md-nav-item>
|
---|
69 | * <md-nav-item md-nav-href="#page2" name="page3">Page Two</md-nav-item>
|
---|
70 | * <md-nav-item md-nav-sref="page3" name="page2">Page Three</md-nav-item>
|
---|
71 | * <md-nav-item
|
---|
72 | * md-nav-sref="app.page4"
|
---|
73 | * sref-opts="{reload: true, notify: true}"
|
---|
74 | * name="page4">
|
---|
75 | * Page Four
|
---|
76 | * </md-nav-item>
|
---|
77 | * </md-nav-bar>
|
---|
78 | *</hljs>
|
---|
79 | * <hljs lang="js">
|
---|
80 | * (function() {
|
---|
81 | * 'use strict';
|
---|
82 | *
|
---|
83 | * $rootScope.$on('$routeChangeSuccess', function(event, current) {
|
---|
84 | * $scope.currentLink = getCurrentLinkFromRoute(current);
|
---|
85 | * });
|
---|
86 | * });
|
---|
87 | * </hljs>
|
---|
88 | */
|
---|
89 | /**
|
---|
90 | * @param $mdAria
|
---|
91 | * @param $mdTheming
|
---|
92 | * @param $window
|
---|
93 | * @param $mdUtil
|
---|
94 | * @constructor
|
---|
95 | * ngInject
|
---|
96 | */
|
---|
97 | function MdNavBar($mdAria, $mdTheming, $window, $mdUtil) {
|
---|
98 | return {
|
---|
99 | restrict: 'E',
|
---|
100 | transclude: true,
|
---|
101 | controller: MdNavBarController,
|
---|
102 | controllerAs: 'ctrl',
|
---|
103 | bindToController: true,
|
---|
104 | scope: {
|
---|
105 | 'mdSelectedNavItem': '=?',
|
---|
106 | 'mdNoInkBar': '=?',
|
---|
107 | 'navBarAriaLabel': '@?',
|
---|
108 | },
|
---|
109 | template:
|
---|
110 | '<div class="md-nav-bar">' +
|
---|
111 | '<nav role="navigation">' +
|
---|
112 | '<ul class="_md-nav-bar-list" ng-transclude role="tablist" ' +
|
---|
113 | 'ng-focus="ctrl.onFocus()" ' + // Deprecated but kept for now in order to not break tests
|
---|
114 | 'aria-label="{{ctrl.navBarAriaLabel}}">' +
|
---|
115 | '</ul>' +
|
---|
116 | '</nav>' +
|
---|
117 | '<md-nav-ink-bar ng-hide="ctrl.mdNoInkBar"></md-nav-ink-bar>' +
|
---|
118 | '</div>',
|
---|
119 | link: function(scope, element, attrs, ctrl) {
|
---|
120 |
|
---|
121 | ctrl.width = $window.innerWidth;
|
---|
122 |
|
---|
123 | function onResize() {
|
---|
124 | if (ctrl.width !== $window.innerWidth) {
|
---|
125 | ctrl.updateSelectedTabInkBar();
|
---|
126 | ctrl.width = $window.innerWidth;
|
---|
127 | scope.$digest();
|
---|
128 | }
|
---|
129 | }
|
---|
130 |
|
---|
131 | function cleanUp() {
|
---|
132 | angular.element($window).off('resize', onResize);
|
---|
133 | }
|
---|
134 |
|
---|
135 | angular.element($window).on('resize', $mdUtil.debounce(onResize, 300));
|
---|
136 | scope.$on('$destroy', cleanUp);
|
---|
137 |
|
---|
138 | $mdTheming(element);
|
---|
139 | if (!ctrl.navBarAriaLabel) {
|
---|
140 | $mdAria.expectAsync(element, 'aria-label', angular.noop);
|
---|
141 | }
|
---|
142 | },
|
---|
143 | };
|
---|
144 | }
|
---|
145 |
|
---|
146 | /**
|
---|
147 | * Controller for the nav-bar component.
|
---|
148 | * Accessibility functionality is implemented as a tablist
|
---|
149 | * (https://www.w3.org/TR/wai-aria-1.0/complete#tablist) and
|
---|
150 | * tabs (https://www.w3.org/TR/wai-aria-1.0/complete#tab).
|
---|
151 | *
|
---|
152 | * @param {!JQLite} $element
|
---|
153 | * @param {!IScope} $scope
|
---|
154 | * @param {!ITimeoutService} $timeout
|
---|
155 | * @param {!Object} $mdConstant
|
---|
156 | * @constructor
|
---|
157 | * @final
|
---|
158 | * ngInject
|
---|
159 | */
|
---|
160 | function MdNavBarController($element, $scope, $timeout, $mdConstant) {
|
---|
161 | // Injected variables
|
---|
162 | /**
|
---|
163 | * @private @const
|
---|
164 | * @type {!ITimeoutService}
|
---|
165 | */
|
---|
166 | this._$timeout = $timeout;
|
---|
167 |
|
---|
168 | /**
|
---|
169 | * @private @const
|
---|
170 | * @type {!IScope}
|
---|
171 | */
|
---|
172 | this._$scope = $scope;
|
---|
173 |
|
---|
174 | /**
|
---|
175 | * @private @const
|
---|
176 | * @type {!Object}
|
---|
177 | */
|
---|
178 | this._$mdConstant = $mdConstant;
|
---|
179 |
|
---|
180 | // Data-bound variables.
|
---|
181 | /** @type {?string} */
|
---|
182 | this.mdSelectedNavItem;
|
---|
183 |
|
---|
184 | /** @type {?string} */
|
---|
185 | this.navBarAriaLabel;
|
---|
186 |
|
---|
187 | // State variables.
|
---|
188 | /** @type {?HTMLElement} */
|
---|
189 | this._navBarEl = $element[0];
|
---|
190 |
|
---|
191 | /** @type {?JQLite} */
|
---|
192 | this._inkbar;
|
---|
193 |
|
---|
194 | var self = this;
|
---|
195 | // need to wait for transcluded content to be available
|
---|
196 | var deregisterTabWatch = this._$scope.$watch(function() {
|
---|
197 | return self._navBarEl.querySelectorAll('._md-nav-button').length;
|
---|
198 | },
|
---|
199 | function(newLength) {
|
---|
200 | if (newLength > 0) {
|
---|
201 | self._initTabs();
|
---|
202 | deregisterTabWatch();
|
---|
203 | }
|
---|
204 | });
|
---|
205 | }
|
---|
206 |
|
---|
207 | /**
|
---|
208 | * Initializes the tab components once they exist.
|
---|
209 | * @private
|
---|
210 | */
|
---|
211 | MdNavBarController.prototype._initTabs = function() {
|
---|
212 | this._inkbar = angular.element(this._navBarEl.querySelector('md-nav-ink-bar'));
|
---|
213 |
|
---|
214 | var self = this;
|
---|
215 | this._$timeout(function() {
|
---|
216 | self._updateTabs(self.mdSelectedNavItem, null);
|
---|
217 | });
|
---|
218 |
|
---|
219 | this._$scope.$watch('ctrl.mdSelectedNavItem', function(newValue, oldValue) {
|
---|
220 | // Wait a digest before update tabs for products doing
|
---|
221 | // anything dynamic in the template.
|
---|
222 | self._$timeout(function() {
|
---|
223 | self._updateTabs(newValue, oldValue);
|
---|
224 | });
|
---|
225 | });
|
---|
226 | };
|
---|
227 |
|
---|
228 | /**
|
---|
229 | * Set the current tab to be selected.
|
---|
230 | * @param {string|undefined} newValue New current tab name.
|
---|
231 | * @param {string|undefined|null} oldValue Previous tab name.
|
---|
232 | * @private
|
---|
233 | */
|
---|
234 | MdNavBarController.prototype._updateTabs = function(newValue, oldValue) {
|
---|
235 | var self = this;
|
---|
236 | var tabs = this._getTabs();
|
---|
237 | var sameTab = newValue === oldValue;
|
---|
238 |
|
---|
239 | // this._getTabs can return null if nav-bar has not yet been initialized
|
---|
240 | if (!tabs) return;
|
---|
241 |
|
---|
242 | var newIndex = -1;
|
---|
243 | var newTab = this._getTabByName(newValue);
|
---|
244 | var oldTab = this._getTabByName(oldValue);
|
---|
245 |
|
---|
246 | if (oldTab) {
|
---|
247 | oldTab.setSelected(false);
|
---|
248 | }
|
---|
249 |
|
---|
250 | if (newTab) {
|
---|
251 | newTab.setSelected(true);
|
---|
252 | newIndex = tabs.indexOf(newTab);
|
---|
253 | }
|
---|
254 |
|
---|
255 | this._$timeout(function() {
|
---|
256 | self._updateInkBarStyles(newTab, newIndex);
|
---|
257 | // Don't change focus when there is no newTab, the new and old tabs are the same, or when
|
---|
258 | // called from MdNavBarController._initTabs() which would have no oldTab defined.
|
---|
259 | if (newTab && oldTab && !sameTab) {
|
---|
260 | self._moveFocus(oldTab, newTab);
|
---|
261 | }
|
---|
262 | });
|
---|
263 | };
|
---|
264 |
|
---|
265 | /**
|
---|
266 | * Repositions the ink bar to the selected tab.
|
---|
267 | * @param {MdNavItemController} tab the nav item that should have ink bar styles applied
|
---|
268 | * @param {number=} newIndex the index of the newly selected nav item
|
---|
269 | * @private
|
---|
270 | */
|
---|
271 | MdNavBarController.prototype._updateInkBarStyles = function(tab, newIndex) {
|
---|
272 | this._inkbar.css({display: newIndex < 0 ? 'none' : ''});
|
---|
273 |
|
---|
274 | if (tab) {
|
---|
275 | var tabEl = tab.getButtonEl();
|
---|
276 | var left = tabEl.offsetLeft;
|
---|
277 | var tabWidth = tabEl.offsetWidth;
|
---|
278 | var navBarWidth = this._navBarEl.getBoundingClientRect().width;
|
---|
279 | var scale = tabWidth / navBarWidth;
|
---|
280 | var translate = left / navBarWidth * 100;
|
---|
281 |
|
---|
282 | this._inkbar.css({ transform: 'translateX(' + translate + '%) scaleX(' + scale + ')' });
|
---|
283 | }
|
---|
284 | };
|
---|
285 |
|
---|
286 | /**
|
---|
287 | * Updates ink bar to match current tab.
|
---|
288 | */
|
---|
289 | MdNavBarController.prototype.updateSelectedTabInkBar = function() {
|
---|
290 | this._updateInkBarStyles(this._getSelectedTab());
|
---|
291 | };
|
---|
292 |
|
---|
293 | /**
|
---|
294 | * Returns an array of the current tabs.
|
---|
295 | * @return {Array<!MdNavItemController>}
|
---|
296 | * @private
|
---|
297 | */
|
---|
298 | MdNavBarController.prototype._getTabs = function() {
|
---|
299 | var controllers = Array.prototype.slice.call(
|
---|
300 | this._navBarEl.querySelectorAll('.md-nav-item'))
|
---|
301 | .map(function(el) {
|
---|
302 | return angular.element(el).controller('mdNavItem');
|
---|
303 | });
|
---|
304 | return controllers.indexOf(undefined) ? controllers : [];
|
---|
305 | };
|
---|
306 |
|
---|
307 | /**
|
---|
308 | * Returns the tab with the specified name.
|
---|
309 | * @param {string} name The name of the tab, found in its name attribute.
|
---|
310 | * @return {MdNavItemController}
|
---|
311 | * @private
|
---|
312 | */
|
---|
313 | MdNavBarController.prototype._getTabByName = function(name) {
|
---|
314 | return this._findTab(function(tab) {
|
---|
315 | return tab.getName() === name;
|
---|
316 | });
|
---|
317 | };
|
---|
318 |
|
---|
319 | /**
|
---|
320 | * Returns the selected tab.
|
---|
321 | * @return {MdNavItemController}
|
---|
322 | * @private
|
---|
323 | */
|
---|
324 | MdNavBarController.prototype._getSelectedTab = function() {
|
---|
325 | return this._findTab(function(tab) {
|
---|
326 | return tab.isSelected();
|
---|
327 | });
|
---|
328 | };
|
---|
329 |
|
---|
330 | /**
|
---|
331 | * Returns the focused tab.
|
---|
332 | * @return {MdNavItemController}
|
---|
333 | */
|
---|
334 | MdNavBarController.prototype.getFocusedTab = function() {
|
---|
335 | return this._findTab(function(tab) {
|
---|
336 | return tab.hasFocus();
|
---|
337 | });
|
---|
338 | };
|
---|
339 |
|
---|
340 | /**
|
---|
341 | * Find a tab that matches the specified function, starting from the first tab.
|
---|
342 | * @param {Function} fn
|
---|
343 | * @param {number=} startIndex index to start at. Defaults to 0.
|
---|
344 | * @returns {MdNavItemController|null}
|
---|
345 | * @private
|
---|
346 | */
|
---|
347 | MdNavBarController.prototype._findTab = function(fn, startIndex) {
|
---|
348 | var tabs = this._getTabs(), i;
|
---|
349 | if (startIndex == null) {
|
---|
350 | startIndex = 0;
|
---|
351 | }
|
---|
352 | for (i = startIndex; i < tabs.length; i++) {
|
---|
353 | if (fn(tabs[i])) {
|
---|
354 | return tabs[i];
|
---|
355 | }
|
---|
356 | }
|
---|
357 | return null;
|
---|
358 | };
|
---|
359 |
|
---|
360 | /**
|
---|
361 | * Find a tab that matches the specified function, going backwards from the end of the list.
|
---|
362 | * @param {Function} fn
|
---|
363 | * @param {number=} startIndex index to start at. Defaults to tabs.length - 1.
|
---|
364 | * @returns {MdNavItemController}
|
---|
365 | * @private
|
---|
366 | */
|
---|
367 | MdNavBarController.prototype._findTabReverse = function(fn, startIndex) {
|
---|
368 | var tabs = this._getTabs();
|
---|
369 | if (startIndex === undefined || startIndex === null) {
|
---|
370 | startIndex = tabs.length - 1;
|
---|
371 | }
|
---|
372 | for (var i = startIndex; i >= 0 ; i--) {
|
---|
373 | if (fn(tabs[i])) {
|
---|
374 | return tabs[i];
|
---|
375 | }
|
---|
376 | }
|
---|
377 | return null;
|
---|
378 | };
|
---|
379 |
|
---|
380 | /**
|
---|
381 | * Direct focus to the selected tab when focus enters the nav bar.
|
---|
382 | */
|
---|
383 | MdNavBarController.prototype.onFocus = function() {
|
---|
384 | var tab = this._getSelectedTab();
|
---|
385 | if (tab && !tab.isFocused) {
|
---|
386 | tab.setFocused(true);
|
---|
387 | }
|
---|
388 | };
|
---|
389 |
|
---|
390 | /**
|
---|
391 | * Move focus from oldTab to newTab.
|
---|
392 | * @param {!MdNavItemController} oldTab
|
---|
393 | * @param {!MdNavItemController} newTab
|
---|
394 | * @private
|
---|
395 | */
|
---|
396 | MdNavBarController.prototype._moveFocus = function(oldTab, newTab) {
|
---|
397 | oldTab.setFocused(false);
|
---|
398 | newTab.setFocused(true);
|
---|
399 | };
|
---|
400 |
|
---|
401 | /**
|
---|
402 | * Focus the first tab.
|
---|
403 | * @private
|
---|
404 | */
|
---|
405 | MdNavBarController.prototype._focusFirstTab = function() {
|
---|
406 | var tabs = this._getTabs();
|
---|
407 | if (!tabs) return;
|
---|
408 | var tabToFocus = this._findTab(function(tab) {
|
---|
409 | return tab._isEnabled();
|
---|
410 | });
|
---|
411 | if (tabToFocus) {
|
---|
412 | this._moveFocus(this.getFocusedTab(), tabToFocus);
|
---|
413 | }
|
---|
414 | };
|
---|
415 |
|
---|
416 | /**
|
---|
417 | * Focus the last tab.
|
---|
418 | * @private
|
---|
419 | */
|
---|
420 | MdNavBarController.prototype._focusLastTab = function() {
|
---|
421 | var tabs = this._getTabs();
|
---|
422 | if (!tabs) return;
|
---|
423 | var tabToFocus = this._findTabReverse(function(tab) {
|
---|
424 | return tab._isEnabled();
|
---|
425 | });
|
---|
426 | if (tabToFocus) {
|
---|
427 | this._moveFocus(this.getFocusedTab(), tabToFocus);
|
---|
428 | }
|
---|
429 | };
|
---|
430 |
|
---|
431 | /**
|
---|
432 | * Focus the next non-disabled tab.
|
---|
433 | * @param {number} focusedTabIndex the index of the currently focused tab
|
---|
434 | * @private
|
---|
435 | */
|
---|
436 | MdNavBarController.prototype._focusNextTab = function(focusedTabIndex) {
|
---|
437 | var tabs = this._getTabs();
|
---|
438 | if (!tabs) return;
|
---|
439 | var tabToFocus = this._findTab(function(tab) {
|
---|
440 | return tab._isEnabled();
|
---|
441 | }, focusedTabIndex + 1);
|
---|
442 | if (tabToFocus) {
|
---|
443 | this._moveFocus(this.getFocusedTab(), tabToFocus);
|
---|
444 | } else {
|
---|
445 | this._focusFirstTab();
|
---|
446 | }
|
---|
447 | };
|
---|
448 |
|
---|
449 | /**
|
---|
450 | * Focus the previous non-disabled tab.
|
---|
451 | * @param {number} focusedTabIndex the index of the currently focused tab
|
---|
452 | * @private
|
---|
453 | */
|
---|
454 | MdNavBarController.prototype._focusPreviousTab = function(focusedTabIndex) {
|
---|
455 | var tabs = this._getTabs();
|
---|
456 | if (!tabs) return;
|
---|
457 | var tabToFocus = this._findTabReverse(function(tab) {
|
---|
458 | return tab._isEnabled();
|
---|
459 | }, focusedTabIndex - 1);
|
---|
460 | if (tabToFocus) {
|
---|
461 | this._moveFocus(this.getFocusedTab(), tabToFocus);
|
---|
462 | } else {
|
---|
463 | this._focusLastTab();
|
---|
464 | }
|
---|
465 | };
|
---|
466 |
|
---|
467 | /**
|
---|
468 | * Responds to keydown events.
|
---|
469 | * Calls to preventDefault() stop the page from scrolling when changing focus in the nav-bar.
|
---|
470 | * @param {!KeyboardEvent} e
|
---|
471 | */
|
---|
472 | MdNavBarController.prototype.onKeydown = function(e) {
|
---|
473 | var keyCodes = this._$mdConstant.KEY_CODE;
|
---|
474 | var tabs = this._getTabs();
|
---|
475 | var focusedTab = this.getFocusedTab();
|
---|
476 | if (!focusedTab || !tabs) return;
|
---|
477 |
|
---|
478 | var focusedTabIndex = tabs.indexOf(focusedTab);
|
---|
479 |
|
---|
480 | // use arrow keys to navigate between tabs
|
---|
481 | switch (e.keyCode) {
|
---|
482 | case keyCodes.UP_ARROW:
|
---|
483 | case keyCodes.LEFT_ARROW:
|
---|
484 | e.preventDefault();
|
---|
485 | this._focusPreviousTab(focusedTabIndex);
|
---|
486 | break;
|
---|
487 | case keyCodes.DOWN_ARROW:
|
---|
488 | case keyCodes.RIGHT_ARROW:
|
---|
489 | e.preventDefault();
|
---|
490 | this._focusNextTab(focusedTabIndex);
|
---|
491 | break;
|
---|
492 | case keyCodes.SPACE:
|
---|
493 | case keyCodes.ENTER:
|
---|
494 | // timeout to avoid a "digest already in progress" console error
|
---|
495 | this._$timeout(function() {
|
---|
496 | focusedTab.getButtonEl().click();
|
---|
497 | });
|
---|
498 | break;
|
---|
499 | case keyCodes.HOME:
|
---|
500 | e.preventDefault();
|
---|
501 | this._focusFirstTab();
|
---|
502 | break;
|
---|
503 | case keyCodes.END:
|
---|
504 | e.preventDefault();
|
---|
505 | this._focusLastTab();
|
---|
506 | break;
|
---|
507 | }
|
---|
508 | };
|
---|
509 |
|
---|
510 | /**
|
---|
511 | * @ngdoc directive
|
---|
512 | * @name mdNavItem
|
---|
513 | * @module material.components.navBar
|
---|
514 | *
|
---|
515 | * @restrict E
|
---|
516 | *
|
---|
517 | * @description
|
---|
518 | * `<md-nav-item>` describes a page navigation link within the `<md-nav-bar>` component.
|
---|
519 | * It renders an `<md-button>` as the actual link.
|
---|
520 | *
|
---|
521 | * Exactly one of the `md-nav-click`, `md-nav-href`, or `md-nav-sref` attributes are required
|
---|
522 | * to be specified.
|
---|
523 | *
|
---|
524 | * @param {string=} nav-item-aria-label Allows setting or overriding the label that is announced by
|
---|
525 | * a screen reader for the nav item's button. If this is not set, the nav item's transcluded
|
---|
526 | * content will be announced. Make sure to set this if the nav item's transcluded content does
|
---|
527 | * not include descriptive text, for example only an icon.
|
---|
528 | * @param {expression=} md-nav-click Expression which will be evaluated when the
|
---|
529 | * link is clicked to change the page. Renders as an `ng-click`.
|
---|
530 | * @param {string=} md-nav-href url to transition to when this link is clicked.
|
---|
531 | * Renders as an `ng-href`.
|
---|
532 | * @param {string=} md-nav-sref UI-Router state to transition to when this link is
|
---|
533 | * clicked. Renders as a `ui-sref`.
|
---|
534 | * @param {string} name The name of this link. Used by the nav bar to know
|
---|
535 | * which link is currently selected.
|
---|
536 | * @param {!object=} sref-opts UI-Router options that are passed to the `$state.go()` function. See
|
---|
537 | * the <a ng-href="https://ui-router.github.io/docs/latest/interfaces/transition.transitionoptions.html"
|
---|
538 | * target="_blank">UI-Router documentation for details</a>.
|
---|
539 | *
|
---|
540 | * @usage
|
---|
541 | * See <a ng-href="api/directive/mdNavBar">md-nav-bar</a> for usage.
|
---|
542 | */
|
---|
543 | /**
|
---|
544 | * @param $mdAria
|
---|
545 | * @param $$rAF
|
---|
546 | * @param $mdUtil
|
---|
547 | * @param $window
|
---|
548 | * @constructor
|
---|
549 | * ngInject
|
---|
550 | */
|
---|
551 | function MdNavItem($mdAria, $$rAF, $mdUtil, $window) {
|
---|
552 | return {
|
---|
553 | restrict: 'E',
|
---|
554 | require: ['mdNavItem', '^mdNavBar'],
|
---|
555 | controller: MdNavItemController,
|
---|
556 | bindToController: true,
|
---|
557 | controllerAs: 'ctrl',
|
---|
558 | replace: true,
|
---|
559 | transclude: true,
|
---|
560 | template: function(tElement, tAttrs) {
|
---|
561 | var hasNavClick = tAttrs.mdNavClick;
|
---|
562 | var hasNavHref = tAttrs.mdNavHref;
|
---|
563 | var hasNavSref = tAttrs.mdNavSref;
|
---|
564 | var hasSrefOpts = tAttrs.srefOpts;
|
---|
565 | var navigationAttribute;
|
---|
566 | var navigationOptions;
|
---|
567 | var buttonTemplate;
|
---|
568 |
|
---|
569 | // Cannot specify more than one nav attribute
|
---|
570 | if ((hasNavClick ? 1 : 0) + (hasNavHref ? 1 : 0) + (hasNavSref ? 1 : 0) > 1) {
|
---|
571 | throw Error(
|
---|
572 | 'Please do not specify more than one of the md-nav-click, md-nav-href, ' +
|
---|
573 | 'or md-nav-sref attributes per nav-item directive.'
|
---|
574 | );
|
---|
575 | }
|
---|
576 |
|
---|
577 | if (hasNavClick !== undefined && hasNavClick !== null) {
|
---|
578 | navigationAttribute = 'ng-click="ctrl.mdNavClick()"';
|
---|
579 | } else if (hasNavHref !== undefined && hasNavHref !== null) {
|
---|
580 | navigationAttribute = 'ng-href="{{ctrl.mdNavHref}}"';
|
---|
581 | } else if (hasNavSref !== undefined && hasNavSref !== null) {
|
---|
582 | navigationAttribute = 'ui-sref="{{ctrl.mdNavSref}}"';
|
---|
583 | } else {
|
---|
584 | throw Error(
|
---|
585 | 'Please specify at least one of the md-nav-click, md-nav-href, or md-nav-sref ' +
|
---|
586 | 'attributes per nav-item directive.');
|
---|
587 | }
|
---|
588 |
|
---|
589 | navigationOptions = hasSrefOpts ? 'ui-sref-opts="{{ctrl.srefOpts}}" ' : '';
|
---|
590 |
|
---|
591 | if (navigationAttribute) {
|
---|
592 | buttonTemplate = '' +
|
---|
593 | '<md-button class="_md-nav-button md-accent" ' +
|
---|
594 | 'ng-class="ctrl.getNgClassMap()" ' +
|
---|
595 | 'ng-blur="ctrl.setFocused(false)" ' +
|
---|
596 | 'ng-disabled="ctrl.disabled" ' +
|
---|
597 | 'tabindex="-1" ' +
|
---|
598 | 'role="tab" ' +
|
---|
599 | 'ng-attr-aria-label="{{ctrl.navItemAriaLabel ? ctrl.navItemAriaLabel : undefined}}" ' +
|
---|
600 | 'aria-selected="{{ctrl.isSelected()}}" ' +
|
---|
601 | navigationOptions +
|
---|
602 | navigationAttribute + '>' +
|
---|
603 | '<span ng-transclude class="_md-nav-button-text"></span>' +
|
---|
604 | '</md-button>';
|
---|
605 | }
|
---|
606 |
|
---|
607 | return '' +
|
---|
608 | '<li class="md-nav-item" ' +
|
---|
609 | 'role="presentation">' +
|
---|
610 | (buttonTemplate || '') +
|
---|
611 | '</li>';
|
---|
612 | },
|
---|
613 | scope: {
|
---|
614 | 'mdNavClick': '&?',
|
---|
615 | 'mdNavHref': '@?',
|
---|
616 | 'mdNavSref': '@?',
|
---|
617 | 'srefOpts': '=?',
|
---|
618 | 'name': '@',
|
---|
619 | 'navItemAriaLabel': '@?',
|
---|
620 | },
|
---|
621 | link: function(scope, element, attrs, controllers) {
|
---|
622 | var disconnect;
|
---|
623 | var mdNavItem;
|
---|
624 | var mdNavBar;
|
---|
625 | var navButton;
|
---|
626 |
|
---|
627 | // When accessing the element's contents synchronously, they
|
---|
628 | // may not be defined yet because of transclusion. There is a higher
|
---|
629 | // chance that it will be accessible if we wait one frame.
|
---|
630 | $$rAF(function() {
|
---|
631 | mdNavItem = controllers[0];
|
---|
632 | mdNavBar = controllers[1];
|
---|
633 | navButton = angular.element(element[0].querySelector('._md-nav-button'));
|
---|
634 |
|
---|
635 | if (!mdNavItem.name) {
|
---|
636 | mdNavItem.name = angular.element(element[0]
|
---|
637 | .querySelector('._md-nav-button-text')).text().trim();
|
---|
638 | }
|
---|
639 |
|
---|
640 | navButton.on('keydown', function($event) {
|
---|
641 | mdNavBar.onKeydown($event);
|
---|
642 | });
|
---|
643 |
|
---|
644 | navButton.on('focus', function() {
|
---|
645 | mdNavItem._focused = true;
|
---|
646 | });
|
---|
647 |
|
---|
648 | navButton.on('click', function() {
|
---|
649 | // This triggers a watcher on mdNavBar.mdSelectedNavItem which calls
|
---|
650 | // MdNavBarController._updateTabs() after a $timeout. That function calls
|
---|
651 | // MdNavItemController.setSelected() for the old tab with false and the new tab with true.
|
---|
652 | mdNavBar.mdSelectedNavItem = mdNavItem.name;
|
---|
653 | scope.$apply();
|
---|
654 | });
|
---|
655 |
|
---|
656 | // Get the disabled attribute value first, then setup observing of value changes
|
---|
657 | mdNavItem.disabled = $mdUtil.parseAttributeBoolean(attrs['disabled'], false);
|
---|
658 | if ('MutationObserver' in $window) {
|
---|
659 | var config = {attributes: true, attributeFilter: ['disabled']};
|
---|
660 | var targetNode = element[0];
|
---|
661 | var mutationCallback = function(mutationList) {
|
---|
662 | $mdUtil.nextTick(function() {
|
---|
663 | mdNavItem.disabled = $mdUtil.parseAttributeBoolean(attrs[mutationList[0].attributeName], false);
|
---|
664 | });
|
---|
665 | };
|
---|
666 | var observer = new MutationObserver(mutationCallback);
|
---|
667 | observer.observe(targetNode, config);
|
---|
668 | disconnect = observer.disconnect.bind(observer);
|
---|
669 | } else {
|
---|
670 | attrs.$observe('disabled', function (value) {
|
---|
671 | mdNavItem.disabled = $mdUtil.parseAttributeBoolean(value, false);
|
---|
672 | });
|
---|
673 | }
|
---|
674 |
|
---|
675 | if (!mdNavItem.navItemAriaLabel) {
|
---|
676 | $mdAria.expectWithText(navButton, 'aria-label');
|
---|
677 | }
|
---|
678 | });
|
---|
679 |
|
---|
680 | scope.$on('destroy', function() {
|
---|
681 | navButton.off('keydown');
|
---|
682 | navButton.off('focus');
|
---|
683 | navButton.off('click');
|
---|
684 | disconnect();
|
---|
685 | });
|
---|
686 | }
|
---|
687 | };
|
---|
688 | }
|
---|
689 |
|
---|
690 | /**
|
---|
691 | * Controller for the nav-item component.
|
---|
692 | * @param {!JQLite} $element
|
---|
693 | * @constructor
|
---|
694 | * @final
|
---|
695 | * ngInject
|
---|
696 | */
|
---|
697 | function MdNavItemController($element) {
|
---|
698 |
|
---|
699 | /**
|
---|
700 | * @private @const
|
---|
701 | * @type {!JQLite}
|
---|
702 | */
|
---|
703 | this._$element = $element;
|
---|
704 |
|
---|
705 | // Data-bound variables
|
---|
706 |
|
---|
707 | /**
|
---|
708 | * @const
|
---|
709 | * @type {?Function}
|
---|
710 | */
|
---|
711 | this.mdNavClick;
|
---|
712 |
|
---|
713 | /**
|
---|
714 | * @const
|
---|
715 | * @type {?string}
|
---|
716 | */
|
---|
717 | this.mdNavHref;
|
---|
718 |
|
---|
719 | /**
|
---|
720 | * @const
|
---|
721 | * @type {?string}
|
---|
722 | */
|
---|
723 | this.mdNavSref;
|
---|
724 | /**
|
---|
725 | * @const
|
---|
726 | * @type {?Object}
|
---|
727 | */
|
---|
728 | this.srefOpts;
|
---|
729 | /**
|
---|
730 | * @const
|
---|
731 | * @type {?string}
|
---|
732 | */
|
---|
733 | this.name;
|
---|
734 |
|
---|
735 | /**
|
---|
736 | * @const
|
---|
737 | * @type {string}
|
---|
738 | */
|
---|
739 | this.navItemAriaLabel;
|
---|
740 |
|
---|
741 | // State variables
|
---|
742 | /**
|
---|
743 | * @private
|
---|
744 | * @type {boolean}
|
---|
745 | */
|
---|
746 | this._selected = false;
|
---|
747 |
|
---|
748 | /**
|
---|
749 | * @type {boolean}
|
---|
750 | */
|
---|
751 | this.isFocused = false;
|
---|
752 | }
|
---|
753 |
|
---|
754 | /**
|
---|
755 | * Returns a map of class names and values for use by ng-class.
|
---|
756 | * @return {!Object<string,boolean>}
|
---|
757 | */
|
---|
758 | MdNavItemController.prototype.getNgClassMap = function() {
|
---|
759 | return {
|
---|
760 | 'md-active': this._selected,
|
---|
761 | 'md-primary': this._selected,
|
---|
762 | 'md-unselected': !this._selected,
|
---|
763 | 'md-focused': this.isFocused,
|
---|
764 | };
|
---|
765 | };
|
---|
766 |
|
---|
767 | /**
|
---|
768 | * Get the name attribute of the tab.
|
---|
769 | * @return {string}
|
---|
770 | */
|
---|
771 | MdNavItemController.prototype.getName = function() {
|
---|
772 | return this.name;
|
---|
773 | };
|
---|
774 |
|
---|
775 | /**
|
---|
776 | * Get the button element associated with the tab.
|
---|
777 | * @return {!Element}
|
---|
778 | */
|
---|
779 | MdNavItemController.prototype.getButtonEl = function() {
|
---|
780 | return this._$element[0].querySelector('._md-nav-button');
|
---|
781 | };
|
---|
782 |
|
---|
783 | /**
|
---|
784 | * Set the selected state of the tab and updates the tabindex.
|
---|
785 | * This function is called for the oldTab and newTab when selection changes.
|
---|
786 | * @param {boolean} isSelected true to select the tab, false to deselect the tab
|
---|
787 | */
|
---|
788 | MdNavItemController.prototype.setSelected = function(isSelected) {
|
---|
789 | this._selected = isSelected;
|
---|
790 | if (isSelected) {
|
---|
791 | // https://www.w3.org/TR/wai-aria-practices/examples/tabs/tabs-2/tabs.html suggests that we call
|
---|
792 | // removeAttribute('tabindex') here, but that causes our unit tests to fail due to
|
---|
793 | // document.activeElement staying set to the body instead of the focused nav button.
|
---|
794 | this.getButtonEl().setAttribute('tabindex', '0');
|
---|
795 | } else {
|
---|
796 | this.getButtonEl().setAttribute('tabindex', '-1');
|
---|
797 | }
|
---|
798 | };
|
---|
799 |
|
---|
800 | /**
|
---|
801 | * @return {boolean}
|
---|
802 | */
|
---|
803 | MdNavItemController.prototype.isSelected = function() {
|
---|
804 | return this._selected;
|
---|
805 | };
|
---|
806 |
|
---|
807 | /**
|
---|
808 | * Set the focused state of the tab.
|
---|
809 | * @param {boolean} isFocused
|
---|
810 | */
|
---|
811 | MdNavItemController.prototype.setFocused = function(isFocused) {
|
---|
812 | this.isFocused = isFocused;
|
---|
813 |
|
---|
814 | if (isFocused) {
|
---|
815 | this.getButtonEl().focus();
|
---|
816 | }
|
---|
817 | };
|
---|
818 |
|
---|
819 | /**
|
---|
820 | * @return {boolean} true if the tab has focus, false if not.
|
---|
821 | */
|
---|
822 | MdNavItemController.prototype.hasFocus = function() {
|
---|
823 | return this.isFocused;
|
---|
824 | };
|
---|
825 |
|
---|
826 | /**
|
---|
827 | * @return {boolean} true if the tab is enabled, false if disabled.
|
---|
828 | * @private
|
---|
829 | */
|
---|
830 | MdNavItemController.prototype._isEnabled = function() {
|
---|
831 | return !this._$element.attr('disabled');
|
---|
832 | };
|
---|
833 |
|
---|
834 | })(window, window.angular); |
---|