1/* 2Copyright © 2013 Adobe Systems Incorporated. 3 4Licensed under the Apache License, Version 2.0 (the âLicenseâ); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an âAS ISâ BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17/** 18 * See <a href="http://jquery.com">http://jquery.com</a>. 19 * @name jquery 20 * @class 21 * See the jQuery Library (<a href="http://jquery.com">http://jquery.com</a>) for full details. This just 22 * documents the function and classes that are added to jQuery by this plug-in. 23 */ 24 25/** 26 * See <a href="http://jquery.com">http://jquery.com</a> 27 * @name fn 28 * @class 29 * See the jQuery Library (<a href="http://jquery.com">http://jquery.com</a>) for full details. This just 30 * documents the function and classes that are added to jQuery by this plug-in. 31 * @memberOf jquery 32 */ 33 34/** 35 * @fileOverview accessibleMegaMenu plugin 36 * 37 *<p>Licensed under the Apache License, Version 2.0 (the âLicenseâ) 38 *<br />Copyright © 2013 Adobe Systems Incorporated. 39 *<br />Project page <a href="https://github.com/adobe-accessibility/Accessible-Mega-Menu">https://github.com/adobe-accessibility/Accessible-Mega-Menu</a> 40 * @version 0.1 41 * @author Michael Jordan 42 * @requires jquery 43 */ 44 45/*jslint browser: true, devel: true, plusplus: true, nomen: true */ 46/*global jQuery, window, document */ 47(function ($, window, document) { 48 "use strict"; 49 var pluginName = "accessibleMegaMenu", 50 defaults = { 51 uuidPrefix: "accessible-megamenu", // unique ID's are required to indicate aria-owns, aria-controls and aria-labelledby 52 menuClass: "accessible-megamenu", // default css class used to define the megamenu styling 53 topNavItemClass: "accessible-megamenu-top-nav-item", // default css class for a top-level navigation item in the megamenu 54 panelClass: "accessible-megamenu-panel", // default css class for a megamenu panel 55 panelGroupClass: "accessible-megamenu-panel-group", // default css class for a group of items within a megamenu panel 56 hoverClass: "hover", // default css class for the hover state 57 focusClass: "focus", // default css class for the focus state 58 openClass: "open", // default css class for the open state, 59 toggleButtonClass: "accessible-megamenu-toggle", // default css class responsive toggle button 60 openDelay: 0, // default open delay when opening menu via mouseover 61 closeDelay: 250, // default open delay when opening menu via mouseover 62 openOnMouseover: false // default setting for whether menu should open on mouseover 63 }, 64 Keyboard = { 65 BACKSPACE: 8, 66 COMMA: 188, 67 DELETE: 46, 68 DOWN: 40, 69 END: 35, 70 ENTER: 13, 71 ESCAPE: 27, 72 HOME: 36, 73 LEFT: 37, 74 PAGE_DOWN: 34, 75 PAGE_UP: 33, 76 PERIOD: 190, 77 RIGHT: 39, 78 SPACE: 32, 79 TAB: 9, 80 UP: 38, 81 keyMap: { 82 48: "0", 83 49: "1", 84 50: "2", 85 51: "3", 86 52: "4", 87 53: "5", 88 54: "6", 89 55: "7", 90 56: "8", 91 57: "9", 92 59: ";", 93 65: "a", 94 66: "b", 95 67: "c", 96 68: "d", 97 69: "e", 98 70: "f", 99 71: "g", 100 72: "h", 101 73: "i", 102 74: "j", 103 75: "k", 104 76: "l", 105 77: "m", 106 78: "n", 107 79: "o", 108 80: "p", 109 81: "q", 110 82: "r", 111 83: "s", 112 84: "t", 113 85: "u", 114 86: "v", 115 87: "w", 116 88: "x", 117 89: "y", 118 90: "z", 119 96: "0", 120 97: "1", 121 98: "2", 122 99: "3", 123 100: "4", 124 101: "5", 125 102: "6", 126 103: "7", 127 104: "8", 128 105: "9", 129 190: "." 130 } 131 }, 132 clearTimeout = window.clearTimeout, 133 setTimeout = window.setTimeout, 134 isOpera = window.opera && window.opera.toString() === '[object Opera]'; 135 /**
136 * @desc Creates a new accessible mega menu instance. 137 * @param {jquery} element 138 * @param {object} [options] Mega Menu options 139 * @param {string} [options.uuidPrefix=accessible-megamenu] - Prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby 140 * @param {string} [options.menuClass=accessible-megamenu] - CSS class used to define the megamenu styling 141 * @param {string} [options.topNavItemClass=accessible-megamenu-top-nav-item] - CSS class for a top-level navigation item in the megamenu 142 * @param {string} [options.panelClass=accessible-megamenu-panel] - CSS class for a megamenu panel 143 * @param {string} [options.panelGroupClass=accessible-megamenu-panel-group] - CSS class for a group of items within a megamenu panel 144 * @param {string} [options.hoverClass=hover] - CSS class for the hover state 145 * @param {string} [options.focusClass=focus] - CSS class for the focus state 146 * @param {string} [options.openClass=open] - CSS class for the open state 147 * @constructor 148 */ 149 function AccessibleMegaMenu(element, options) { 150 this.element = element; 151 152 // merge optional settings and defaults into settings 153 this.settings = $.extend({}, defaults, options); 154 155 this._defaults = defaults; 156 this._name = pluginName; 157 158 this.mouseTimeoutID = null; 159 this.focusTimeoutID = null; 160 this.mouseFocused = false; 161 this.justFocused = false; 162 163 this.init(); 164 } 165 166 AccessibleMegaMenu.prototype = (function () { 167 168 /* private attributes and methods ------------------------ */ 169 var uuid = 0, 170 keydownTimeoutDuration = 1000, 171 keydownSearchString = "", 172 isTouch = 'ontouchstart' in window || window.navigator.msMaxTouchPoints, 173 _getPlugin, 174 _addUniqueId, 175 _togglePanel, 176 _clickHandler, 177 _touchmoveHandler, 178 _clickOutsideHandler, 179 _DOMAttrModifiedHandler, 180 _focusInHandler, 181 _focusOutHandler, 182 _keyDownHandler, 183 _mouseDownHandler, 184 _mouseOverHandler, 185 _mouseOutHandler, 186 _clickToggleHandler, 187 _toggleExpandedEventHandlers, 188 _addEventHandlers, 189 _removeEventHandlers; 190 191 /** 192 * @name jQuery.fn.accessibleMegaMenu~_getPlugin 193 * @desc Returns the parent accessibleMegaMenu instance for a given element 194 * @param {jQuery} element 195 * @memberof jQuery.fn.accessibleMegaMenu 196 * @inner 197 * @private 198 */ 199 _getPlugin = function (element) { 200 return $(element).closest(':data(plugin_' + pluginName + ')').data("plugin_" + pluginName); 201 }; 202 203 /** 204 * @name jQuery.fn.accessibleMegaMenu~_addUniqueId 205 * @desc Adds a unique id and element. 206 * The id string starts with the 207 * string defined in settings.uuidPrefix. 208 * @param {jQuery} element 209 * @memberof jQuery.fn.accessibleMegaMenu 210 * @inner 211 * @private 212 */ 213 _addUniqueId = function (element) { 214 element = $(element); 215 var settings = this.settings; 216 if (!element.attr("id")) { 217 element.attr("id", settings.uuidPrefix + "-" + new Date().getTime() + "-" + (++uuid)); 218 } 219 }; 220 221 /** 222 * @name jQuery.fn.accessibleMegaMenu~_togglePanel 223 * @desc Toggle the display of mega menu panels in response to an event. 224 * The optional boolean value 'hide' forces all panels to hide. 225 * @param {event} event 226 * @param {Boolean} [hide] Hide all mega menu panels when true 227 * @memberof jQuery.fn.accessibleMegaMenu 228 * @inner 229 * @private 230 */ 231 _togglePanel = function (event, hide) { 232 var target = $(event.target), 233 that = this, 234 settings = this.settings, 235 menu = this.menu, 236 topli = target.closest('.' + settings.topNavItemClass), 237 panel = target.hasClass(settings.panelClass) ? target : target.closest('.' + settings.panelClass), 238 newfocus; 239 240 _toggleExpandedEventHandlers.call(this, true); 241 242 // Hide all panels. 243 if (hide) { 244 // Get the first top level menu item. 245 topli = menu.find('.' + settings.topNavItemClass + ' .' + settings.openClass + ':first').closest('.' + settings.topNavItemClass); 246 // Validate event. 247 if (!(topli.is(event.relatedTarget) || topli.has(event.relatedTarget).length > 0)) { 248 if ((event.type === 'mouseout' || event.type === 'focusout') && topli.has(document.activeElement).length > 0) { 249 return; 250 } 251 252 // Close top level link. 253 topli.find('[aria-expanded]') 254 .attr('aria-expanded', 'false') 255 .removeClass(settings.openClass) 256 // Close panel. 257 topli.find('.' + settings.panelClass) 258 .removeClass(settings.openClass) 259 .attr('aria-hidden', 'true'); 260 261 if ((event.type === 'keydown' && event.keyCode === Keyboard.ESCAPE) || event.type === 'DOMAttrModified') { 262 newfocus = topli.find(':tabbable:first'); 263 setTimeout(function () { 264 menu.find('[aria-expanded].' + that.settings.panelClass).off('DOMAttrModified.accessible-megamenu'); 265 newfocus.focus(); 266 that.justFocused = false;
267 }, 99); 268 } 269 } else if (topli.length === 0) { 270 menu.find('[aria-expanded=true]') 271 .attr('aria-expanded', 'false') 272 .removeClass(settings.openClass) 273 .closest('.' + settings.panelClass) 274 .removeClass(settings.openClass) 275 .attr('aria-hidden', 'true'); 276 } 277 } 278 // Toggle panels. 279 else { 280 clearTimeout(that.focusTimeoutID); 281 // Close previously open top level link and its panel. 282 var openli = menu.find('[aria-expanded=true]').parent(); 283 if (!openli.is(topli)) { 284 openli.find('[aria-expanded]') 285 .attr('aria-expanded', 'false') 286 .removeClass(settings.openClass) 287 .siblings('.' + settings.panelClass) 288 .removeClass(settings.openClass) 289 .attr('aria-hidden', 'true'); 290 } 291 // Open current top level link and its panel. 292 topli.find('[aria-expanded]') 293 .attr('aria-expanded', 'true') 294 .addClass(settings.openClass) 295 .siblings('.' + settings.panelClass) 296 .addClass(settings.openClass) 297 .attr('aria-hidden', 'false'); 298 299 if (topli.length) { 300 var pageScrollPosition = $('html')[0].scrollTop; 301 var openPanelTopPosition = topli.offset().top; 302 303 if (pageScrollPosition > openPanelTopPosition) { 304 $('html')[0].scrollTop = openPanelTopPosition; 305 } 306 } 307 308 if (event.type === 'mouseover' && target.is(':tabbable') && topli.length === 1 && panel.length === 0 && menu.has(document.activeElement).length > 0) { 309 target.focus(); 310 that.justFocused = false; 311 } 312 313 _toggleExpandedEventHandlers.call(that); 314 } 315 }; 316 317 /** 318 * @name jQuery.fn.accessibleMegaMenu~_clickHandler 319 * @desc Handle click event on mega menu item 320 * @param {event} Event object 321 * @memberof jQuery.fn.accessibleMegaMenu 322 * @inner 323 * @private 324 */ 325 _clickHandler = function (event) { 326 var target = $(event.target).closest(':tabbable'), 327 topli = target.closest('.' + this.settings.topNavItemClass), 328 panel = target.closest('.' + this.settings.panelClass);
329 // With panel. 330 if (topli.length === 1 && panel.length === 0 && topli.find('.' + this.settings.panelClass).length === 1) { 331 // Handle click event. 332 if (!target.hasClass(this.settings.openClass)) { 333 event.preventDefault(); 334 event.stopPropagation(); 335 _togglePanel.call(this, event); 336 this.justFocused = false; 337 } 338 else { 339 // Handle focus event. 340 if (this.justFocused) { 341 event.preventDefault(); 342 event.stopPropagation(); 343 this.justFocused = false; 344 } 345 // Handle touch/click event. 346 else if (isTouch || !isTouch && !this.settings.openOnMouseover) { 347 event.preventDefault(); 348 event.stopPropagation(); 349 _togglePanel.call(this, event, target.hasClass(this.settings.openClass)); 350 } 351 } 352 }
353 // Without panel on enter event. 354 else if (topli.length === 1 && panel.length === 0 && event.type == "keydown") { 355 window.location.href = target.attr("href"); 356 } 357 }; 358 359 /** 360 * @name jQuery.fn.accessibleMegaMenu~_touchmoveHandler 361 * @desc Handle touch move event on menu 362 * @memberof jQuery.fn.accessibleMegaMenu 363 * @inner 364 * @private 365 */ 366 _touchmoveHandler = function () { 367 this.justMoved = true; 368 }; 369 370 /** 371 * @name jQuery.fn.accessibleMegaMenu~_clickOutsideHandler 372 * @desc Handle click event outside of a the megamenu 373 * @param {event} Event object 374 * @memberof jQuery.fn.accessibleMegaMenu 375 * @inner 376 * @private 377 */ 378 _clickOutsideHandler = function (event) { 379 if ($(event.target).closest(this.menu).length === 0) { 380 event.preventDefault(); 381 event.stopPropagation(); 382 _togglePanel.call(this, event, true); 383 } 384 }; 385 386 /** 387 * @name jQuery.fn.accessibleMegaMenu~_DOMAttrModifiedHandler 388 * @desc Handle DOMAttrModified event on panel to respond to Windows 8 Narrator ExpandCollapse pattern 389 * @param {event} Event object 390 * @memberof jQuery.fn.accessibleMegaMenu 391 * @inner 392 * @private 393 */ 394 _DOMAttrModifiedHandler = function (event) { 395 if (event.originalEvent.attrName === 'aria-expanded' 396 && event.originalEvent.newValue === 'false' 397 && $(event.target).hasClass(this.settings.openClass)) { 398 event.preventDefault(); 399 event.stopPropagation(); 400 _togglePanel.call(this, event, true); 401 } 402 }; 403 404 /** 405 * @name jQuery.fn.accessibleMegaMenu~_focusInHandler 406 * @desc Handle focusin event on mega menu item. 407 * @param {event} Event object 408 * @memberof jQuery.fn.accessibleMegaMenu 409 * @inner 410 * @private 411 */ 412 _focusInHandler = function (event) { 413 clearTimeout(this.focusTimeoutID); 414 var target = $(event.target), 415 panel = target.closest('.' + this.settings.panelClass); 416 target 417 .addClass(this.settings.focusClass); 418 this.justFocused = !this.mouseFocused || (!this.settings.openOnMouseover && this.mouseFocused); 419 this.mouseFocused = false; 420 if (this.justFocused && this.panels.not(panel).filter('.' + this.settings.openClass).length) { 421 _togglePanel.call(this, event); 422 } 423 }; 424 425 /** 426 * @name jQuery.fn.accessibleMegaMenu~_focusOutHandler 427 * @desc Handle focusout event on mega menu item. 428 * @param {event} Event object 429 * @memberof jQuery.fn.accessibleMegaMenu 430 * @inner 431 * @private 432 */ 433 _focusOutHandler = function (event) { 434 this.justFocused = false; 435 var that = this, 436 target = $(event.target), 437 topli = target.closest('.' + this.settings.topNavItemClass); 438 target 439 .removeClass(this.settings.focusClass); 440 441 if (typeof window.cvox === 'object' && typeof window.cvox.Api !== 'object') { 442 // If ChromeVox is running... 443 that.focusTimeoutID = setTimeout(function () { 444 window.cvox.Api.getCurrentNode(function (node) { 445 if (topli.has(node).length) { 446 // and the current node being voiced is in 447 // the mega menu, clearTimeout, 448 // so the panel stays open. 449 clearTimeout(that.focusTimeoutID); 450 } else { 451 that.focusTimeoutID = setTimeout(function (scope, event, hide) { 452 _togglePanel.call(scope, event, hide); 453 }, 275, that, event, true); 454 } 455 }); 456 }, 25); 457 } else { 458 that.focusTimeoutID = setTimeout(function () { 459 if (that.mouseFocused && event.relatedTarget === null) { 460 return; 461 } 462 _togglePanel.call(that, event, true); 463 }, 300); 464 } 465 }; 466 467 /** 468 * @name jQuery.fn.accessibleMegaMenu~_keyDownHandler
469 * @desc Handle keydown event on mega menu. 470 * @param {event} Event object 471 * @memberof jQuery.fn.accessibleMegaMenu 472 * @inner 473 * @private 474 */ 475 _keyDownHandler = function (event) { 476 var that = (this.constructor === AccessibleMegaMenu) ? this : _getPlugin(this), // determine the AccessibleMegaMenu plugin instance 477 settings = that.settings, 478 target = $($(this).is('.' + settings.hoverClass + ':tabbable') ? this : event.target), // if the element is hovered the target is this, otherwise, its the focused element 479 menu = that.menu, 480 topnavitems = that.topnavitems, 481 topli = target.closest('.' + settings.topNavItemClass), 482 tabbables = menu.find(':tabbable'), 483 panel = target.hasClass(settings.panelClass) ? target : target.closest('.' + settings.panelClass), 484 panelGroups = panel.find('.' + settings.panelGroupClass), 485 currentPanelGroup = target.closest('.' + settings.panelGroupClass), 486 next, 487 keycode = event.keyCode || event.which, 488 start, 489 i, 490 o, 491 label, 492 found = false, 493 newString = Keyboard.keyMap[event.keyCode] || '', 494 regex, 495 isTopNavItem = (topli.length === 1 && panel.length === 0); 496
497 if (target.is("input:focus, select:focus, textarea:focus, button:focus")) { 498 // if the event target is a form element we should handle keydown normally 499 return; 500 } 501 502 if (target.is('.' + settings.hoverClass + ':tabbable')) { 503 $('html').off('keydown.accessible-megamenu'); 504 } 505 506 switch (keycode) { 507 case Keyboard.ESCAPE: 508 this.mouseFocused = false; 509 _togglePanel.call(that, event, true); 510 break; 511 case Keyboard.DOWN: 512 event.preventDefault(); 513 this.mouseFocused = false; 514 if (isTopNavItem) { 515 _togglePanel.call(that, event); 516 found = (topli.find('.' + settings.panelClass + ' :tabbable:first').focus().length === 1); 517 } else { 518 found = (tabbables.filter(':gt(' + tabbables.index(target) + '):first').focus().length === 1); 519 } 520 521 if (!found && isOpera && (event.ctrlKey || event.metaKey)) { 522 tabbables = $(':tabbable'); 523 i = tabbables.index(target); 524 found = ($(':tabbable:gt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 525 } 526 break; 527 case Keyboard.UP: 528 event.preventDefault(); 529 this.mouseFocused = false; 530 if (isTopNavItem && target.hasClass(settings.openClass)) { 531 _togglePanel.call(that, event, true); 532 next = topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last'); 533 if (next.children('.' + settings.panelClass).length) { 534 found = (next.find('[aria-expanded]') 535 .attr('aria-expanded', 'true') 536 .addClass(settings.openClass) 537 .filter('.' + settings.panelClass) 538 .attr('aria-hidden', 'false') 539 .find(':tabbable:last') 540 .focus() === 1); 541 } 542 } else if (!isTopNavItem) { 543 found = (tabbables.filter(':lt(' + tabbables.index(target) + '):last').focus().length === 1); 544 } 545 546 if (!found && isOpera && (event.ctrlKey || event.metaKey)) { 547 tabbables = $(':tabbable'); 548 i = tabbables.index(target); 549 found = ($(':tabbable:lt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 550 } 551 break; 552 case Keyboard.RIGHT: 553 event.preventDefault(); 554 this.mouseFocused = false; 555 if (isTopNavItem) { 556 found = (topnavitems.filter(':gt(' + topnavitems.index(topli) + '):first').find(':tabbable:first').focus().length === 1); 557 } else { 558 if (panelGroups.length && currentPanelGroup.length) { 559 // if the current panel contains panel groups, and we are able to focus the first tabbable element of the next panel group 560 found = (panelGroups.filter(':gt(' + panelGroups.index(currentPanelGroup) + '):first').find(':tabbable:first').focus().length === 1); 561 } 562 563 if (!found) { 564 found = (topli.find(':tabbable:first').focus().length === 1); 565 } 566 } 567 break; 568 case Keyboard.LEFT: 569 event.preventDefault(); 570 this.mouseFocused = false; 571 if (isTopNavItem) { 572 found = (topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last').find(':tabbable:first').focus().length === 1); 573 } else { 574 if (panelGroups.length && currentPanelGroup.length) { 575 // if the current panel contains panel groups, and we are able to focus the first tabbable element of the previous panel group 576 found = (panelGroups.filter(':lt(' + panelGroups.index(currentPanelGroup) + '):last').find(':tabbable:first').focus().length === 1); 577 } 578 579 if (!found) { 580 found = (topli.find(':tabbable:first').focus().length === 1); 581 } 582 } 583 break; 584 case Keyboard.TAB: 585 this.mouseFocused = false;
586 i = tabbables.index(target); 587 if (event.shiftKey && isTopNavItem && target.hasClass(settings.openClass)) { 588 _togglePanel.call(that, event, true); 589 next = topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last'); 590 if (next.children('.' + settings.panelClass).length) { 591 found = next.children() 592 .attr('aria-expanded', 'true') 593 .addClass(settings.openClass) 594 .filter('.' + settings.panelClass) 595 .attr('aria-hidden', 'false') 596 .find(':tabbable:last') 597 .focus(); 598 } 599 } else if (event.shiftKey && i > 0) { 600 found = (tabbables.filter(':lt(' + i + '):last').focus().length === 1); 601 } else if (!event.shiftKey && i < tabbables.length - 1) { 602 found = (tabbables.filter(':gt(' + i + '):first').focus().length === 1); 603 } else if (isOpera) { 604 tabbables = $(':tabbable'); 605 i = tabbables.index(target); 606 if (event.shiftKey) { 607 found = ($(':tabbable:lt(' + $(':tabbable').index(target) + '):last').focus().length === 1); 608 } else { 609 found = ($(':tabbable:gt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 610 } 611 } 612 613 if (found) { 614 event.preventDefault(); 615 } 616 break; 617 case Keyboard.SPACE: 618 case Keyboard.ENTER: 619 // Top level links. 620 if (isTopNavItem) { 621 event.preventDefault(); 622 // Handle enter event on open top level link as escape event. 623 if (target.hasClass("open")) { 624 this.mouseFocused = false; 625 _togglePanel.call(that, event, true); 626 } 627 // Handle enter event on top level link as a click. 628 else { 629 _clickHandler.call(that, event); 630 } 631 } 632 // Sub level links. 633 else { 634 return true; 635 } 636 break; 637 default: 638 // alphanumeric filter 639 clearTimeout(this.keydownTimeoutID); 640 641 keydownSearchString += newString !== keydownSearchString ? newString : ''; 642 643 if (keydownSearchString.length === 0) { 644 return; 645 } 646 647 this.keydownTimeoutID = setTimeout(function () { 648 keydownSearchString = ''; 649 }, keydownTimeoutDuration); 650 651 if (isTopNavItem && !target.hasClass(settings.openClass)) { 652 tabbables = tabbables.filter(':not(.' + settings.panelClass + ' :tabbable)'); 653 } else { 654 tabbables = topli.find(':tabbable'); 655 } 656 657 if (event.shiftKey) { 658 tabbables = $(tabbables.get() 659 .reverse()); 660 } 661 662 for (i = 0; i < tabbables.length; i++) { 663 o = tabbables.eq(i); 664 if (o.is(target)) { 665 start = (keydownSearchString.length === 1) ? i + 1 : i; 666 break; 667 } 668 } 669 670 regex = new RegExp('^' + keydownSearchString.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'), 'i'); 671 672 for (i = start; i < tabbables.length; i++) { 673 o = tabbables.eq(i); 674 label = $.trim(o.text()); 675 if (regex.test(label)) { 676 found = true; 677 o.focus(); 678 break; 679 } 680 } 681 if (!found) { 682 for (i = 0; i < start; i++) { 683 o = tabbables.eq(i); 684 label = $.trim(o.text()); 685 if (regex.test(label)) { 686 o.focus(); 687 break; 688 } 689 } 690 } 691 break; 692 } 693 that.justFocused = false;
694 }; 695 696 /** 697 * @name jQuery.fn.accessibleMegaMenu~_mouseDownHandler 698 * @desc Handle mousedown event on mega menu. 699 * @param {event} Event object 700 * @memberof accessibleMegaMenu 701 * @inner 702 * @private 703 */ 704 _mouseDownHandler = function (event) { 705 if ($(event.target).closest(this.settings.panelClass) || $(event.target).closest(":focusable").length) { 706 this.mouseFocused = true; 707 if ($(event.target).closest(this.settings.menuClass)) { 708 $('html').on('keydown.accessible-megamenu', $.proxy(_keyDownHandler, event.target)); 709 } 710 } 711 clearTimeout(this.mouseTimeoutID); 712 this.mouseTimeoutID = setTimeout(function () { 713 clearTimeout(this.focusTimeoutID); 714 }, 1); 715 }; 716 717 /** 718 * @name jQuery.fn.accessibleMegaMenu~_mouseOverHandler 719 * @desc Handle mouseover event on mega menu. 720 * @param {event} Event object 721 * @memberof jQuery.fn.accessibleMegaMenu 722 * @inner 723 * @private 724 */ 725 _mouseOverHandler = function (event) { 726 clearTimeout(this.mouseTimeoutID); 727 var that = this; 728 729 if (!that.settings.openOnMouseover) { 730 return; 731 } 732 733 this.mouseTimeoutID = setTimeout(function () { 734 $(event.target).addClass(that.settings.hoverClass); 735 _togglePanel.call(that, event); 736 if ($(event.target).closest(that.settings.menuClass)) { 737 $('html').on('keydown.accessible-megamenu', $.proxy(_keyDownHandler, event.target)); 738 } 739 }, this.settings.openDelay); 740 }; 741 742 /** 743 * @name jQuery.fn.accessibleMegaMenu~_mouseOutHandler 744 * @desc Handle mouseout event on mega menu. 745 * @param {event} Event object 746 * @memberof jQuery.fn.accessibleMegaMenu 747 * @inner 748 * @private 749 */ 750 _mouseOutHandler = function (event) { 751 clearTimeout(this.mouseTimeoutID); 752 var that = this; 753 754 if (!that.settings.openOnMouseover) { 755 return; 756 } 757 758 $(event.target) 759 .removeClass(that.settings.hoverClass); 760 761 that.mouseTimeoutID = setTimeout(function () { 762 _togglePanel.call(that, event, true); 763 }, this.settings.closeDelay); 764 if ($(event.target).is(':tabbable')) { 765 $('html').off('keydown.accessible-megamenu'); 766 } 767 }; 768 769 /** 770 * @name jQuery.fn.accessibleMegaMenu~_clickToggleHandler 771 * @desc Handle click event on menu toggle button. 772 * @memberof jQuery.fn.accessibleMegaMenu 773 * @inner 774 * @private 775 */ 776 _clickToggleHandler = function () { 777 var isExpanded = this.toggleButton.attr('aria-expanded') === 'true'; 778 this.toggleButton.attr({'aria-expanded': !isExpanded}); 779 }; 780 781 _toggleExpandedEventHandlers = function (hide) { 782 var menu = this.menu; 783 if (hide) { 784 $('html').off('mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-accessible-megamenu, pointerup.outside-accessible-megamenu'); 785 786 menu.find('[aria-expanded].' + this.settings.panelClass).off('DOMAttrModified.accessible-megamenu'); 787 } else { 788 $('html').on('mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-accessible-megamenu, pointerup.outside-accessible-megamenu', $.proxy(_clickOutsideHandler, this)); 789 790 /* Narrator in Windows 8 automatically toggles the aria-expanded property on double tap or click. 791 To respond to the change to collapse the panel, we must add a listener for a DOMAttrModified event. */ 792 menu.find('[aria-expanded=true].' + this.settings.panelClass).on('DOMAttrModified.accessible-megamenu', $.proxy(_DOMAttrModifiedHandler, this)); 793 } 794 }; 795 796 _addEventHandlers = function() { 797 var menu = this.menu, 798 toggleButton = this.toggleButton; 799 menu.on("focusin.accessible-megamenu", ":focusable, ." + this.settings.panelClass, $.proxy(_focusInHandler, this)) 800 .on("focusout.accessible-megamenu", ":focusable, ." + this.settings.panelClass, $.proxy(_focusOutHandler, this))
801 .on("keydown.accessible-megamenu", $.proxy(_keyDownHandler, this)) 802 .on("mouseover.accessible-megamenu", $.proxy(_mouseOverHandler, this)) 803 .on("mouseout.accessible-megamenu", $.proxy(_mouseOutHandler, this)) 804 .on("mousedown.accessible-megamenu", $.proxy(_mouseDownHandler, this)) 805 .on("click.accessible-megamenu", $.proxy(_clickHandler, this)); 806 807 toggleButton.on('click.accessible-megamenu', $.proxy(_clickToggleHandler, this)); 808 809 if (isTouch) { 810 menu.on("touchmove.accessible-megamenu", $.proxy(_touchmoveHandler, this)); 811 } 812 813 if ($(document.activeElement).closest(menu).length) { 814 $(document.activeElement).trigger("focusin.accessible-megamenu"); 815 } 816 }; 817 818 _removeEventHandlers = function () { 819 var menu = this.menu, 820 toggleButton = this.toggleButton; 821 menu.off('.accessible-megamenu'); 822 if (menu.find('[aria-expanded=true].' + this.settings.panelClass).length) { 823 _toggleExpandedEventHandlers.call(this, true); 824 } 825 826 toggleButton.off('.accessible-megamenu'); 827 }; 828 829 /* public attributes and methods ------------------------- */ 830 return { 831 constructor: AccessibleMegaMenu, 832 833 /** 834 * @lends jQuery.fn.accessibleMegaMenu 835 * @desc Initializes an instance of the accessibleMegaMenu plugins 836 * @memberof jQuery.fn.accessibleMegaMenu 837 * @instance 838 */ 839 init: function () { 840 var settings = this.settings, 841 nav = $(this.element), 842 menu = nav.children('ol,ul').first(), 843 topnavitems = menu.children(), 844 toggleButton = nav.children('button').first(); 845 this.start(settings, nav, menu, topnavitems, toggleButton); 846 }, 847 848 start: function(settings, nav, menu, topnavitems, toggleButton) { 849 var that = this; 850 this.settings = settings; 851 this.menu = menu; 852 this.topnavitems = topnavitems; 853 this.toggleButton = toggleButton; 854 855 nav.attr("role", "navigation"); 856 _addUniqueId.call(that, menu); 857 menu.addClass(settings.menuClass); 858 menu.addClass(['js', settings.menuClass].join('-')); 859 topnavitems.each(function (i, topnavitem) { 860 var topnavitemlink, topnavitempanel; 861 topnavitem = $(topnavitem); 862 topnavitem.addClass(settings.topNavItemClass); 863 topnavitemlink = topnavitem.find("a").first(); 864 topnavitempanel = topnavitem.find('.' + settings.panelClass); 865 _addUniqueId.call(that, topnavitemlink); 866 // When sub nav exists. 867 if (topnavitempanel.length) { 868 _addUniqueId.call(that, topnavitempanel); 869 // Add attributes to top level link. 870 topnavitemlink.attr({ 871 "role": "button", 872 "aria-controls": topnavitempanel.attr("id"), 873 "aria-expanded": false, 874 "tabindex": 0 875 }); 876 // Add attributes to sub nav. 877 topnavitempanel.attr({ 878 "role": "region", 879 "aria-hidden": true 880 }) 881 .addClass(settings.panelClass) 882 .not("[aria-labelledby]") 883 .attr("aria-labelledby", topnavitemlink.attr("id")); 884 } 885 }); 886 887 this.panels = menu.find("." + settings.panelClass); 888 889 menu.find("hr").attr("role", "separator"); 890 891 toggleButton.addClass(settings.toggleButtonClass); 892 toggleButton.attr({'aria-expanded': false, 'aria-controls': menu.attr('id')}); 893 894 _addEventHandlers.call(this); 895 }, 896 897 /**
898 * @desc Removes maga menu javascript behavior 899 * @example $(selector).accessibleMegaMenu("destroy"); 900 * @return {object} 901 * @memberof jQuery.fn.accessibleMegaMenu 902 * @instance 903 */ 904 destroy: function () { 905 this.menu.removeClass(['js', this.settings.menuClass].join('-')); 906 _removeEventHandlers.call(this, true); 907 }, 908 909 /** 910 * @desc Get default values 911 * @example $(selector).accessibleMegaMenu("getDefaults"); 912 * @return {object} 913 * @memberof jQuery.fn.accessibleMegaMenu 914 * @instance 915 */ 916 getDefaults: function () { 917 return this._defaults; 918 }, 919 920 /** 921 * @desc Get any option set to plugin using its name (as string) 922 * @example $(selector).accessibleMegaMenu("getOption", some_option); 923 * @param {string} opt 924 * @return {string} 925 * @memberof jQuery.fn.accessibleMegaMenu 926 * @instance 927 */ 928 getOption: function (opt) { 929 return this.settings[opt]; 930 }, 931 932 /** 933 * @desc Get all options 934 * @example $(selector).accessibleMegaMenu("getAllOptions"); 935 * @return {object} 936 * @memberof jQuery.fn.accessibleMegaMenu 937 * @instance 938 */ 939 getAllOptions: function () { 940 return this.settings; 941 }, 942 943 /** 944 * @desc Set option 945 * @example $(selector).accessibleMegaMenu("setOption", "option_name", "option_value", reinitialize); 946 * @param {string} opt - Option name 947 * @param {string} val - Option value 948 * @param {boolean} [reinitialize] - boolean to re-initialize the menu. 949 * @memberof jQuery.fn.accessibleMegaMenu 950 * @instance 951 */ 952 setOption: function (opt, value, reinitialize) { 953 this.settings[opt] = value; 954 if (reinitialize) { 955 this.init(); 956 } 957 } 958 }; 959 }()); 960 961 /* lightweight plugin wrapper around the constructor, 962 to prevent against multiple instantiations */ 963 964 /** 965 * @class accessibleMegaMenu 966 * @memberOf jQuery.fn 967 * @classdesc Implements an accessible mega menu as a jQuery plugin. 968 * <p>The mega-menu It is modeled after the mega menu on {@link http://adobe.com|adobe.com} but has been simplified for use by others. A brief description of the interaction design choices can be found in a blog post at {@link http://blogs.adobe.com/accessibility/2013/05/adobe-com.html|Mega menu accessibility on adobe.com}.</p> 969 * <h3>Keyboard Accessibility</h3> 970 * <p>The accessible mega menu supports keyboard interaction modeled after the behavior described in the {@link http://www.w3.org/TR/wai-aria-practices/#menu|WAI-ARIA Menu or Menu bar (widget) design pattern}, however we also try to respect users' general expectations for the behavior of links in a global navigation. To this end, the accessible mega menu implementation permits tab focus on each of the six top-level menu items. When one of the menu items has focus, pressing the Enter key, Spacebar or Down arrow will open the submenu panel, and pressing the Left or Right arrow key will shift focus to the adjacent menu item. Links within the submenu panels are included in the tab order when the panel is open. They can also be navigated with the arrow keys or by typing the first character in the link name, which speeds up keyboard navigation considerably. Pressing the Escape key closes the submenu and restores focus to the parent menu item.</p> 971 * <h3>Screen Reader Accessibility</h3> 972 * <p>The accessible mega menu models its use of WAI-ARIA Roles, States, and Properties after those described in the {@link http://www.w3.org/TR/wai-aria-practices/#menu|WAI-ARIA Menu or Menu bar (widget) design pattern} with some notable exceptions, so that it behaves better with screen reader user expectations for global navigation. We don't use <code class="prettyprint prettyprinted" style=""><span class="pln">role</span><span class="pun">=</span><span class="str">"menu"</span></code> for the menu container and <code class="prettyprint prettyprinted" style=""><span class="pln">role</span><span class="pun">=</span><span class="str">"menuitem"</span></code> for each of the links therein, because if we do, assistive technology will no longer interpret the links as links, but instead, as menu items, and the links in our global navigation will no longer show up when a screen reader user executes a shortcut command to bring up a list of links in the page.</p> 973 * @example <h4>HTML</h4><hr/> 974 <nav> 975 <ul class="nav-menu"> 976 <li class="nav-item"> 977 <a href="?movie">Movies</a> 978 <div class="sub-nav"> 979 <ul class="sub-nav-group"> 980 <li><a href="?movie&genre=0">Action &amp; Adventure</a></li> 981 <li><a href="?movie&genre=2">Children &amp; Family</a></li> 982 <li>&#8230;</li> 983 </ul> 984 <ul class="sub-nav-group"> 985 <li><a href="?movie&genre=7">Dramas</a></li> 986 <li><a href="?movie&genre=9">Foreign</a></li> 987 <li>&#8230;</li> 988 </ul> 989 <ul class="sub-nav-group"> 990 <li><a href="?movie&genre=14">Musicals</a></li> 991 <li><a href="?movie&genre=15">Romance</a></li> 992 <li>&#8230;</li> 993 </ul> 994 </div> 995 </li> 996 <li class="nav-item"> 997 <a href="?tv">TV Shows</a> 998 <div class="sub-nav"> 999 <ul class="sub-nav-group"> 1000 <li><a href="?tv&genre=20">Classic TV</a></li> 1001 <li><a href="?tv&genre=21">Crime TV</a></li> 1002 <li>&#8230;</li> 1003 </ul> 1004 <ul class="sub-nav-group"> 1005 <li><a href="?tv&genre=27">Reality TV</a></li> 1006 <li><a href="?tv&genre=30">TV Action</a></li> 1007 <li>&#8230;</li> 1008 </ul> 1009 <ul class="sub-nav-group"> 1010 <li><a href="?tv&genre=33">TV Dramas</a></li> 1011 <li><a href="?tv&genre=34">TV Horror</a></li> 1012 <li>&#8230;</li> 1013 </ul> 1014 </div> 1015 </li> 1016 </ul> 1017 </nav> 1018 * @example <h4>CSS</h4><hr/> 1019 /* Rudimentary mega menu CSS for demonstration */ 1020 1021 /* mega menu list */ 1022 .nav-menu { 1023 display: block; 1024 position: relative; 1025 list-style: none; 1026 margin: 0; 1027 padding: 0; 1028 z-index: 15; 1029} 1030 1031 /* a top level navigation item in the mega menu */ 1032 .nav-item { 1033 list-style: none; 1034 display: inline-block; 1035 padding: 0; 1036 margin: 0; 1037} 1038 1039 /* first descendant link within a top level navigation item */ 1040 .nav-item > a { 1041 position: relative; 1042 display: inline-block; 1043 padding: 0.5em 1em; 1044 margin: 0 0 -1px 0; 1045 border: 1px solid transparent; 1046} 1047 1048 /* focus/open states of first descendant link within a top level 1049 navigation item */ 1050 .nav-item > a:focus, 1051 .nav-item > a.open { 1052 border: 1px solid #dedede; 1053} 1054 1055 /* open state of first descendant link within a top level 1056 navigation item */ 1057 .nav-item > a.open { 1058 background-color: #fff; 1059 border-bottom: none; 1060 z-index: 1; 1061} 1062 1063 /* sub-navigation panel */ 1064 .sub-nav { 1065 position: absolute; 1066 display: none; 1067 top: 2.2em;
1068 margin-top: -1px; 1069 padding: 0.5em 1em; 1070 border: 1px solid #dedede; 1071 background-color: #fff; 1072} 1073 1074 /* sub-navigation panel open state */ 1075 .sub-nav.open { 1076 display: block; 1077} 1078 1079 /* list of items within sub-navigation panel */ 1080 .sub-nav ul { 1081 display: inline-block; 1082 vertical-align: top; 1083 margin: 0 1em 0 0; 1084 padding: 0; 1085} 1086 1087 /* list item within sub-navigation panel */ 1088 .sub-nav li { 1089 display: block; 1090 list-style-type: none; 1091 margin: 0; 1092 padding: 0; 1093} 1094 * @example <h4>JavaScript</h4><hr/> 1095 <!-- include jquery --> 1096 <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script> 1097 1098 <!-- include the jquery-accessibleMegaMenu plugin script --> 1099 <script src="js/jquery-accessibleMegaMenu.js"></script> 1100 1101 <!-- initialize a selector as an accessibleMegaMenu --> 1102 <script> 1103 $("nav:first").accessibleMegaMenu({ 1104 /* prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby */ 1105 uuidPrefix: "accessible-megamenu", 1106 1107 /* css class used to define the megamenu styling */ 1108 menuClass: "nav-menu", 1109 1110 /* css class for a top-level navigation item in the megamenu */ 1111 topNavItemClass: "nav-item", 1112 1113 /* css class for a megamenu panel */ 1114 panelClass: "sub-nav", 1115 1116 /* css class for a group of items within a megamenu panel */ 1117 panelGroupClass: "sub-nav-group", 1118 1119 /* css class for the hover state */ 1120 hoverClass: "hover", 1121 1122 /* css class for the focus state */ 1123 focusClass: "focus", 1124 1125 /* css class for the open state */ 1126 openClass: "open" 1127 }); 1128 </script> 1129 * @param {object} [options] Mega Menu options 1130 * @param {string} [options.uuidPrefix=accessible-megamenu] - Prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby 1131 * @param {string} [options.menuClass=accessible-megamenu] - CSS class used to define the megamenu styling 1132 * @param {string} [options.topNavItemClass=accessible-megamenu-top-nav-item] - CSS class for a top-level navigation item in the megamenu 1133 * @param {string} [options.panelClass=accessible-megamenu-panel] - CSS class for a megamenu panel 1134 * @param {string} [options.panelGroupClass=accessible-megamenu-panel-group] - CSS class for a group of items within a megamenu panel 1135 * @param {string} [options.hoverClass=hover] - CSS class for the hover state 1136 * @param {string} [options.focusClass=focus] - CSS class for the focus state 1137 * @param {string} [options.openClass=open] - CSS class for the open state 1138 * @param {string} [options.openDelay=0] - Open delay when opening menu via mouseover 1139 * @param {string} [options.closeDelay=250] - Open delay when opening menu via mouseover 1140 * @param {boolean} [options.openOnMouseover=false] - Should menu open on mouseover 1141 */ 1142 $.fn[pluginName] = function (options) { 1143 return this.each(function () { 1144 var pluginInstance = $.data(this, "plugin_" + pluginName); 1145 if (!pluginInstance) { 1146 $.data(this, "plugin_" + pluginName, new $.fn[pluginName].Acce
1146ssibleMegaMenu(this, options)); 1147 } else if (typeof pluginInstance[options] === 'function') { 1148 pluginInstance[options].apply(pluginInstance, Array.prototype.slice.call(arguments, 1)); 1149 } 1150 }); 1151 }; 1152 1153 $.fn[pluginName].AccessibleMegaMenu = AccessibleMegaMenu; 1154 1155 /* :focusable and :tabbable selectors from 1156 https://raw.github.com/jquery/jquery-ui/master/ui/jquery.ui.core.js */ 1157 1158 /** 1159 * @private 1160 */ 1161 function visible(element) { 1162 return $.expr.filters.visible(element) && !$(element).parents().addBack().filter(function () { 1163 return $.css(this, "visibility") === "hidden"; 1164 }).length; 1165 } 1166 1167 /** 1168 * @private 1169 */ 1170 function focusable(element, isTabIndexNotNaN) { 1171 var map, mapName, img, 1172 nodeName = element.nodeName.toLowerCase(); 1173 if ("area" === nodeName) { 1174 map = element.parentNode; 1175 mapName = map.name; 1176 if (!element.href || !mapName || map.nodeName.toLowerCase() !== "map") { 1177 return false; 1178 } 1179 img = $("img[usemap=#" + mapName + "]")[0]; 1180 return !!img && visible(img); 1181 } 1182 return (/input|select|textarea|button|object/.test(nodeName) ? !element.disabled : 1183 "a" === nodeName ? 1184 element.href || isTabIndexNotNaN : 1185 isTabIndexNotNaN) && 1186 // the element and all of its ancestors must be visible 1187 visible(element); 1188 } 1189 1190 $.extend($.expr[":"], { 1191 data: $.expr.createPseudo ? $.expr.createPseudo(function (dataName) { 1192 return function (elem) { 1193 return !!$.data(elem, dataName); 1194 }; 1195 }) : // support: jQuery <1.8 1196 function (elem, i, match) { 1197 return !!$.data(elem, match[3]); 1198 }, 1199 1200 focusable: function (element) { 1201 return focusable(element, !isNaN($.attr(element, "tabindex"))); 1202 }, 1203 1204 tabbable: function (element) { 1205 var tabIndex = $.attr(element, "tabindex"), 1206 isTabIndexNaN = isNaN(tabIndex); 1207 return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); 1208 } 1209 }); 1210}(jQuery, window, document));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.