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 */ 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 openDelay: 0, // default open delay when opening menu via mouseover 60 closeDelay: 250 // default open delay when opening menu via mouseover 61 }, 62 Keyboard = { 63 BACKSPACE: 8, 64 COMMA: 188, 65 DELETE: 46, 66 DOWN: 40, 67 END: 35, 68 ENTER: 13, 69 ESCAPE: 27, 70 HOME: 36, 71 LEFT: 37, 72 PAGE_DOWN: 34, 73 PAGE_UP: 33, 74 PERIOD: 190, 75 RIGHT: 39, 76 SPACE: 32, 77 TAB: 9, 78 UP: 38, 79 keyMap: { 80 48: "0", 81 49: "1", 82 50: "2", 83 51: "3", 84 52: "4", 85 53: "5", 86 54: "6", 87 55: "7", 88 56: "8", 89 57: "9", 90 59: ";", 91 65: "a", 92 66: "b", 93 67: "c", 94 68: "d", 95 69: "e", 96 70: "f", 97 71: "g", 98 72: "h", 99 73: "i", 100 74: "j", 101 75: "k", 102 76: "l", 103 77: "m", 104 78: "n", 105 79: "o", 106 80: "p", 107 81: "q", 108 82: "r", 109 83: "s", 110 84: "t", 111 85: "u", 112 86: "v", 113 87: "w", 114 88: "x", 115 89: "y", 116 90: "z", 117 96: "0", 118 97: "1", 119 98: "2", 120 99: "3", 121 100: "4", 122 101: "5", 123 102: "6", 124 103: "7", 125 104: "8", 126 105: "9", 127 190: "." 128 } 129 }; 130 /**
131 * @desc Creates a new accessible mega menu instance. 132 * @param {jquery} element 133 * @param {object} [options] Mega Menu options 134 * @param {string} [options.uuidPrefix=accessible-megamenu] - Prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby 135 * @param {string} [options.menuClass=accessible-megamenu] - CSS class used to define the megamenu styling 136 * @param {string} [options.topNavItemClass=accessible-megamenu-top-nav-item] - CSS class for a top-level navigation item in the megamenu 137 * @param {string} [options.panelClass=accessible-megamenu-panel] - CSS class for a megamenu panel 138 * @param {string} [options.panelGroupClass=accessible-megamenu-panel-group] - CSS class for a group of items within a megamenu panel 139 * @param {string} [options.hoverClass=hover] - CSS class for the hover state 140 * @param {string} [options.focusClass=focus] - CSS class for the focus state 141 * @param {string} [options.openClass=open] - CSS class for the open state 142 * @constructor 143 */ 144 function AccessibleMegaMenu(element, options) { 145 this.element = element; 146 147 // merge optional settings and defaults into settings 148 this.settings = $.extend({}, defaults, options); 149 150 this._defaults = defaults; 151 this._name = pluginName; 152 153 this.mouseTimeoutID = null; 154 this.focusTimeoutID = null; 155 this.mouseFocused = false; 156 this.justFocused = false; 157 158 this.init(); 159 } 160 161 AccessibleMegaMenu.prototype = (function () { 162 163 /* private attributes and methods ------------------------ */ 164 var uuid = 0, 165 keydownTimeoutDuration = 1000, 166 keydownSearchString = "", 167 isTouch = typeof window.hasOwnProperty === "function" && !!window.hasOwnProperty("ontouchstart"), 168 _getPlugin, 169 _addUniqueId, 170 _togglePanel, 171 _clickHandler, 172 _clickOutsideHandler, 173 _DOMAttrModifiedHandler, 174 _focusInHandler, 175 _focusOutHandler, 176 _keyDownHandler, 177 _mouseDownHandler, 178 _mouseOverHandler, 179 _mouseOutHandler, 180 _toggleExpandedEventHandlers; 181 182 /** 183 * @name jQuery.fn.accessibleMegaMenu~_getPlugin 184 * @desc Returns the parent accessibleMegaMenu instance for a given element 185 * @param {jQuery} element 186 * @memberof jQuery.fn.accessibleMegaMenu 187 * @inner 188 * @private 189 */ 190 _getPlugin = function (element) { 191 return $(element).closest(':data(plugin_' + pluginName + ')').data("plugin_" + pluginName); 192 }; 193 194 /** 195 * @name jQuery.fn.accessibleMegaMenu~_addUniqueId 196 * @desc Adds a unique id and element. 197 * The id string starts with the 198 * string defined in settings.uuidPrefix. 199 * @param {jQuery} element 200 * @memberof jQuery.fn.accessibleMegaMenu 201 * @inner 202 * @private 203 */ 204 _addUniqueId = function (element) { 205 element = $(element); 206 var settings = this.settings; 207 if (!element.attr("id")) { 208 element.attr("id", settings.uuidPrefix + "-" + new Date().getTime() + "-" + (++uuid)); 209 } 210 }; 211 212 /** 213 * @name jQuery.fn.accessibleMegaMenu~_togglePanel 214 * @desc Toggle the display of mega menu panels in response to an event. 215 * The optional boolean value 'hide' forces all panels to hide. 216 * @param {event} event 217 * @param {Boolean} [hide] Hide all mega menu panels when true 218 * @memberof jQuery.fn.accessibleMegaMenu 219 * @inner 220 * @private 221 */ 222 _togglePanel = function (event, hide) { 223 var target = $(event.target), 224 that = this, 225 settings = this.settings, 226 menu = this.menu, 227 topli = target.closest('.' + settings.topNavItemClass), 228 panel = target.hasClass(settings.panelClass) ? target : target.closest('.' + settings.panelClass), 229 newfocus; 230 231 _toggleExpandedEventHandlers.call(this, true); 232 233 if (hide) { 234 topli = menu.find('.' + settings.topNavItemClass + ' .' + settings.openClass + ':first').closest('.' + settings.topNavItemClass); 235 if (!(topli.is(event.relatedTarget) || topli.has(event.relatedTarget).length > 0)) { 236 if ((event.type === 'mouseout' || event.type === 'focusout') && topli.has(document.activeElement).length > 0) { 237 return; 238 } 239 topli.find('[aria-expanded]') 240 .attr('aria-expanded', 'false') 241 .removeClass(settings.openClass) 242 .filter('.' + settings.panelClass) 243 .attr('aria-hidden', 'true'); 244 if ((event.type === 'keydown' && event.keyCode === Keyboard.ESCAPE) || event.type === 'DOMAttrModified') { 245 newfocus = topli.find(':tabbable:first'); 246 setTimeout(function () { 247 menu.find('[aria-expanded].' + that.settings.panelClass).off('DOMAttrModified.accessible-megamenu'); 248 newfocus.focus(); 249 that.justFocused = false;
250 }, 99); 251 } 252 } else if (topli.length === 0) { 253 menu.find('[aria-expanded=true]') 254 .attr('aria-expanded', 'false') 255 .removeClass(settings.openClass) 256 .filter('.' + settings.panelClass) 257 .attr('aria-hidden', 'true'); 258 } 259 } else { 260 clearTimeout(that.focusTimeoutID); 261 topli.siblings() 262 .find('[aria-expanded]') 263 .attr('aria-expanded', 'false') 264 .removeClass(settings.openClass) 265 .filter('.' + settings.panelClass) 266 .attr('aria-hidden', 'true'); 267 topli.find('[aria-expanded]') 268 .attr('aria-expanded', 'true') 269 .addClass(settings.openClass) 270 .filter('.' + settings.panelClass) 271 .attr('aria-hidden', 'false'); 272 if (event.type === 'mouseover' && target.is(':tabbable') && topli.length === 1 && panel.length === 0 && menu.has(document.activeElement).length > 0) { 273 target.focus(); 274 that.justFocused = false; 275 } 276 277 _toggleExpandedEventHandlers.call(that); 278 } 279 }; 280 281 /** 282 * @name jQuery.fn.accessibleMegaMenu~_clickHandler 283 * @desc Handle click event on mega menu item 284 * @param {event} Event object 285 * @memberof jQuery.fn.accessibleMegaMenu 286 * @inner 287 * @private 288 */ 289 _clickHandler = function (event) { 290 var target = $(event.target).closest(':tabbable'), 291 topli = target.closest('.' + this.settings.topNavItemClass), 292 panel = target.closest('.' + this.settings.panelClass); 293 if (topli.length === 1 294 && panel.length === 0 295 && topli.find('.' + this.settings.panelClass).length === 1) { 296 if (!target.hasClass(this.settings.openClass)) { 297 event.preventDefault(); 298 event.stopPropagation(); 299 _togglePanel.call(this, event); 300 this.justFocused = false; 301 } else { 302 if (this.justFocused) { 303 event.preventDefault(); 304 event.stopPropagation(); 305 this.justFocused = false; 306 } else if (isTouch) { 307 event.preventDefault(); 308 event.stopPropagation(); 309 _togglePanel.call(this, event, target.hasClass(this.settings.openClass)); 310 } 311 } 312 } 313 }; 314 315 /** 316 * @name jQuery.fn.accessibleMegaMenu~_clickOutsideHandler 317 * @desc Handle click event outside of a the megamenu 318 * @param {event} Event object 319 * @memberof jQuery.fn.accessibleMegaMenu 320 * @inner 321 * @private 322 */ 323 _clickOutsideHandler = function (event) { 324 if ($(event.target).closest(this.menu).length === 0) { 325 event.preventDefault(); 326 event.stopPropagation(); 327 _togglePanel.call(this, event, true); 328 } 329 }; 330 331 /** 332 * @name jQuery.fn.accessibleMegaMenu~_DOMAttrModifiedHandler 333 * @desc Handle DOMAttrModified event on panel to respond to Windows 8 Narrator ExpandCollapse pattern 334 * @param {event} Event object 335 * @memberof jQuery.fn.accessibleMegaMenu 336 * @inner 337 * @private 338 */ 339 _DOMAttrModifiedHandler = function (event) { 340 if (event.originalEvent.attrName === 'aria-expanded' 341 && event.originalEvent.newValue === 'false' 342 && $(event.target).hasClass(this.settings.openClass)) { 343 event.preventDefault(); 344 event.stopPropagation(); 345 _togglePanel.call(this, event, true); 346 } 347 }; 348 349 /** 350 * @name jQuery.fn.accessibleMegaMenu~_focusInHandler
351 * @desc Handle focusin event on mega menu item. 352 * @param {event} Event object 353 * @memberof jQuery.fn.accessibleMegaMenu 354 * @inner 355 * @private 356 */ 357 _focusInHandler = function (event) { 358 clearTimeout(this.focusTimeoutID); 359 var target = $(event.target), 360 panel = target.closest('.' + this.settings.panelClass); 361 target 362 .addClass(this.settings.focusClass) 363 .on('click.accessible-megamenu', $.proxy(_clickHandler, this)); 364 this.justFocused = !this.mouseFocused; 365 this.mouseFocused = false; 366 if (this.panels.not(panel).filter('.' + this.settings.openClass).length) { 367 _togglePanel.call(this, event); 368 } 369 }; 370 371 /** 372 * @name jQuery.fn.accessibleMegaMenu~_focusOutHandler 373 * @desc Handle focusout event on mega menu item. 374 * @param {event} Event object 375 * @memberof jQuery.fn.accessibleMegaMenu 376 * @inner 377 * @private 378 */ 379 _focusOutHandler = function (event) { 380 this.justFocused = false; 381 var that = this, 382 target = $(event.target), 383 topli = target.closest('.' + this.settings.topNavItemClass), 384 keepOpen = false; 385 target 386 .removeClass(this.settings.focusClass) 387 .off('click.accessible-megamenu'); 388 389 if (window.cvox) { 390 // If ChromeVox is running... 391 that.focusTimeoutID = setTimeout(function () { 392 window.cvox.Api.getCurrentNode(function (node) { 393 if (topli.has(node).length) { 394 // and the current node being voiced is in 395 // the mega menu, clearTimeout, 396 // so the panel stays open. 397 clearTimeout(that.focusTimeoutID); 398 } else { 399 that.focusTimeoutID = setTimeout(function (scope, event, hide) { 400 _togglePanel.call(scope, event, hide); 401 }, 275, that, event, true); 402 } 403 }); 404 }, 25); 405 } else { 406 that.focusTimeoutID = setTimeout(function () { 407 _togglePanel.call(that, event, true); 408 }, 300); 409 } 410 }; 411 412 /** 413 * @name jQuery.fn.accessibleMegaMenu~_keyDownHandler 414 * @desc Handle keydown event on mega menu. 415 * @param {event} Event object 416 * @memberof jQuery.fn.accessibleMegaMenu 417 * @inner 418 * @private 419 */ 420 _keyDownHandler = function (event) { 421 var that = (this.constructor === AccessibleMegaMenu) ? this : _getPlugin(this), // determine the AccessibleMegaMenu plugin instance 422 settings = that.settings, 423 target = $($(this).is('.' + settings.hoverClass + ':tabbable') ? this : event.target), // if the element is hovered the target is this, otherwise, its the focused element 424 menu = that.menu, 425 topnavitems = that.topnavitems, 426 topli = target.closest('.' + settings.topNavItemClass), 427 tabbables = menu.find(':tabbable'), 428 panel = target.hasClass(settings.panelClass) ? target : target.closest('.' + settings.panelClass), 429 panelGroups = panel.find('.' + settings.panelGroupClass), 430 currentPanelGroup = target.closest('.' + settings.panelGroupClass), 431 next, 432 keycode = event.keyCode || event.which, 433 start, 434 i, 435 o, 436 label, 437 found = false, 438 newString = Keyboard.keyMap[event.keyCode] || '', 439 regex, 440 isTopNavItem = (topli.length === 1 && panel.length === 0); 441
442 if (target.is("input:focus, select:focus, textarea:focus, button:focus")) { 443 // if the event target is a form element we should handle keydown normally 444 return; 445 } 446 447 if (target.is('.' + settings.hoverClass + ':tabbable')) { 448 $('html').off('keydown.accessible-megamenu'); 449 } 450 451 switch (keycode) { 452 case Keyboard.ESCAPE: 453 _togglePanel.call(that, event, true); 454 break; 455 case Keyboard.DOWN: 456 event.preventDefault(); 457 if (isTopNavItem) { 458 _togglePanel.call(that, event); 459 found = (topli.find('.' + settings.panelClass + ' :tabbable:first').focus().length === 1); 460 } else { 461 found = (tabbables.filter(':gt(' + tabbables.index(target) + '):first').focus().length === 1); 462 } 463 464 if (!found && window.opera && opera.toString() === "[object Opera]" && (event.ctrlKey || event.metaKey)) { 465 tabbables = $(':tabbable'); 466 i = tabbables.index(target); 467 found = ($(':tabbable:gt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 468 } 469 break; 470 case Keyboard.UP: 471 event.preventDefault(); 472 if (isTopNavItem && target.hasClass(settings.openClass)) { 473 _togglePanel.call(that, event, true); 474 next = topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last'); 475 if (next.children('.' + settings.panelClass).length) { 476 found = (next.children() 477 .attr('aria-expanded', 'true') 478 .addClass(settings.openClass) 479 .filter('.' + settings.panelClass) 480 .attr('aria-hidden', 'false') 481 .find(':tabbable:last') 482 .focus() === 1); 483 } 484 } else if (!isTopNavItem) { 485 found = (tabbables.filter(':lt(' + tabbables.index(target) + '):last').focus().length === 1); 486 } 487 488 if (!found && window.opera && opera.toString() === "[object Opera]" && (event.ctrlKey || event.metaKey)) { 489 tabbables = $(':tabbable'); 490 i = tabbables.index(target); 491 found = ($(':tabbable:lt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 492 } 493 break; 494 case Keyboard.RIGHT: 495 event.preventDefault(); 496 if (isTopNavItem) { 497 found = (topnavitems.filter(':gt(' + topnavitems.index(topli) + '):first').find(':tabbable:first').focus().length === 1); 498 } else { 499 if (panelGroups.length && currentPanelGroup.length) { 500 // if the current panel contains panel groups, and we are able to focus the first tabbable element of the next panel group 501 found = (panelGroups.filter(':gt(' + panelGroups.index(currentPanelGroup) + '):first').find(':tabbable:first').focus().length === 1); 502 } 503 504 if (!found) { 505 found = (topli.find(':tabbable:first').focus().length === 1); 506 } 507 } 508 break; 509 case Keyboard.LEFT: 510 event.preventDefault(); 511 if (isTopNavItem) { 512 found = (topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last').find(':tabbable:first').focus().length === 1); 513 } else { 514 if (panelGroups.length && currentPanelGroup.length) { 515 // if the current panel contains panel groups, and we are able to focus the first tabbable element of the previous panel group 516 found = (panelGroups.filter(':lt(' + panelGroups.index(currentPanelGroup) + '):last').find(':tabbable:first').focus().length === 1); 517 } 518 519 if (!found) { 520 found = (topli.find(':tabbable:first').focus().length === 1); 521 } 522 } 523 break; 524 case Keyboard.TAB: 525 i = tabbables.index(target); 526 if (event.shiftKey && isTopNavItem && target.hasClass(settings.openClass)) { 527 _togglePanel.call(that, event, true); 528 next = topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last'); 529 if (next.children('.' + settings.panelClass).length) { 530 found = next.children() 531 .attr('aria-expanded', 'true') 532 .addClass(settings.openClass) 533 .filter('.' + settings.panelClass) 534 .attr('aria-hidden', 'false') 535 .find(':tabbable:last') 536 .focus(); 537 } 538 } else if (event.shiftKey && i > 0) { 539 found = (tabbables.filter(':lt(' + i + '):last').focus().length === 1); 540 } else if (!event.shiftKey && i < tabbables.length - 1) { 541 found = (tabbables.filter(':gt(' + i + '):first').focus().length === 1); 542 } else if (window.opera && opera.toString() === "[object Opera]") { 543 tabbables = $(':tabbable'); 544 i = tabbables.index(target); 545 if (event.shiftKey) { 546 found = ($(':tabbable:lt(' + $(':tabbable').index(target) + '):last').focus().length === 1); 547 } else { 548 found = ($(':tabbable:gt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 549 } 550 } 551 552 if (found) { 553 event.preventDefault(); 554 } 555 break; 556 case Keyboard.SPACE: 557 if (isTopNavItem) { 558 event.preventDefault(); 559 _clickHandler.call(that, event); 560 } else { 561 return true; 562 } 563 break; 564 case Keyboard.ENTER: 565 return true; 566 break; 567 default: 568 // alphanumeric filter 569 clearTimeout(this.keydownTimeoutID); 570 571 keydownSearchString += newString !== keydownSearchString ? newString : ''; 572 573 if (keydownSearchString.length === 0) { 574 return; 575 } 576 577 this.keydownTimeoutID = setTimeout(function () { 578 keydownSearchString = ''; 579 }, keydownTimeoutDuration); 580 581 if (isTopNavItem && !target.hasClass(settings.openClass)) { 582 tabbables = tabbables.filter(':not(.' + settings.panelClass + ' :tabbable)'); 583 } else { 584 tabbables = topli.find(':tabbable'); 585 } 586 587 if (event.shiftKey) { 588 tabbables = $(tabbables.get() 589 .reverse()); 590 } 591 592 for (i = 0; i < tabbables.length; i++) { 593 o = tabbables.eq(i); 594 if (o.is(target)) { 595 start = (keydownSearchString.length === 1) ? i + 1 : i; 596 break; 597 } 598 } 599 600 regex = new RegExp('^' + keydownSearchString.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'), 'i'); 601 602 for (i = start; i < tabbables.length; i++) { 603 o = tabbables.eq(i); 604 label = $.trim(o.text()); 605 if (regex.test(label)) { 606 found = true; 607 o.focus(); 608 break; 609 } 610 } 611 if (!found) { 612 for (i = 0; i < start; i++) { 613 o = tabbables.eq(i); 614 label = $.trim(o.text()); 615 if (regex.test(label)) { 616 o.focus(); 617 break; 618 } 619 } 620 } 621 break; 622 } 623 that.justFocused = false;
624 }; 625 626 /** 627 * @name jQuery.fn.accessibleMegaMenu~_mouseDownHandler 628 * @desc Handle mousedown event on mega menu. 629 * @param {event} Event object 630 * @memberof accessibleMegaMenu 631 * @inner 632 * @private 633 */ 634 _mouseDownHandler = function (event) { 635 if ($(event.target).is(this.settings.panelClass) || $(event.target).closest(":focusable").length) { 636 this.mouseFocused = true; 637 } 638 clearTimeout(this.mouseTimeoutID); 639 this.mouseTimeoutID = setTimeout(function () { 640 clearTimeout(this.focusTimeoutID); 641 }, 1); 642 }; 643 644 /** 645 * @name jQuery.fn.accessibleMegaMenu~_mouseOverHandler 646 * @desc Handle mouseover event on mega menu. 647 * @param {event} Event object 648 * @memberof jQuery.fn.accessibleMegaMenu 649 * @inner 650 * @private 651 */ 652 _mouseOverHandler = function (event) { 653 clearTimeout(this.mouseTimeoutID); 654 var that = this; 655 this.mouseTimeoutID = setTimeout(function () { 656 $(event.target).addClass(that.settings.hoverClass); 657 _togglePanel.call(that, event); 658 if ($(event.target).is(':tabbable')) { 659 $('html').on('keydown.accessible-megamenu', $.proxy(_keyDownHandler, event.target)); 660 } 661 }, this.settings.openDelay); 662 }; 663 664 /** 665 * @name jQuery.fn.accessibleMegaMenu~_mouseOutHandler 666 * @desc Handle mouseout event on mega menu. 667 * @param {event} Event object 668 * @memberof jQuery.fn.accessibleMegaMenu 669 * @inner 670 * @private 671 */ 672 _mouseOutHandler = function (event) { 673 clearTimeout(this.mouseTimeoutID); 674 var that = this; 675 $(event.target) 676 .removeClass(that.settings.hoverClass); 677 678 that.mouseTimeoutID = setTimeout(function () { 679 _togglePanel.call(that, event, true); 680 }, this.settings.closeDelay); 681 if ($(event.target).is(':tabbable')) { 682 $('html').off('keydown.accessible-megamenu'); 683 } 684 }; 685 686 _toggleExpandedEventHandlers = function (hide) { 687 var menu = this.menu; 688 if (hide) { 689 $('html').off('mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-accessible-megamenu, pointerup.outside-accessible-megamenu'); 690 691 menu.find('[aria-expanded].' + this.settings.panelClass).off('DOMAttrModified.accessible-megamenu'); 692 } else { 693 $('html').on('mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-accessible-megamenu, pointerup.outside-accessible-megamenu', $.proxy(_clickOutsideHandler, this)); 694 695 /* Narrator in Windows 8 automatically toggles the aria-expanded property on double tap or click. 696 To respond to the change to collapse the panel, we must add a listener for a DOMAttrModified event. */ 697 menu.find('[aria-expanded=true].' + this.settings.panelClass).on('DOMAttrModified.accessible-megamenu', $.proxy(_DOMAttrModifiedHandler, this)); 698 } 699 }; 700 701 /* public attributes and methods ------------------------- */ 702 return { 703 constructor: AccessibleMegaMenu, 704 705 /** 706 * @lends jQuery.fn.accessibleMegaMenu 707 * @desc Initializes an instance of the accessibleMegaMenu plugins 708 * @memberof jQuery.fn.accessibleMegaMenu 709 * @instance 710 */ 711 init: function () { 712 var settings = this.settings, 713 nav = $(this.element), 714 menu = nav.children().first(), 715 topnavitems = menu.children(); 716 this.start(settings, nav, menu, topnavitems); 717 }, 718 719 start: function(settings, nav, menu, topnavitems) { 720 var that = this; 721 this.settings = settings; 722 this.menu = menu; 723 this.topnavitems = topnavitems; 724 725 nav.attr("role", "navigation"); 726 menu.addClass(settings.menuClass); 727 topnavitems.each(function (i, topnavitem) { 728 var topnavitemlink, topnavitempanel; 729 topnavitem = $(topnavitem); 730 topnavitem.addClass(settings.topNavItemClass); 731 topnavitemlink = topnavitem.find(":tabbable:first"); 732 topnavitempanel = topnavitem.children(":not(:tabbable):last"); 733 _addUniqueId.call(that, topnavitemlink); 734 if (topnavitempanel.length) { 735 _addUniqueId.call(that, topnavitempanel); 736 topnavitemlink.attr({ 737 // "aria-haspopup": true, 738 "aria-controls": topnavitempanel.attr("id"), 739 "aria-expanded": false 740 }); 741 742 topnavitempanel.attr({ 743 "role": "region", 744 "aria-expanded": false, 745 "aria-hidden": true 746 }) 747 .addClass(settings.panelClass) 748 .not("[aria-labelledby]") 749 .attr("aria-labelledby", topnavitemlink.attr("id")); 750 } 751 }); 752 753 this.panels = menu.find("." + settings.panelClass); 754 755 menu.on("focusin.accessible-megamenu", ":focusable, ." + settings.panelClass, $.proxy(_focusInHandler, this)) 756 .on("focusout.accessible-megamenu", ":focusable, ." + settings.panelClass, $.proxy(_focusOutHandler, this))
757 .on("keydown.accessible-megamenu", $.proxy(_keyDownHandler, this)) 758 .on("mouseover.accessible-megamenu", $.proxy(_mouseOverHandler, this)) 759 .on("mouseout.accessible-megamenu", $.proxy(_mouseOutHandler, this)) 760 .on("mousedown.accessible-megamenu", $.proxy(_mouseDownHandler, this)); 761 762 if (isTouch) { 763 menu.on("touchstart.accessible-megamenu", $.proxy(_clickHandler, this)); 764 } 765 766 menu.find("hr").attr("role", "separator"); 767 768 if ($(document.activeElement).closest(menu).length) { 769 $(document.activeElement).trigger("focusin.accessible-megamenu"); 770 } 771 }, 772 773 /** 774 * @desc Get default values 775 * @example $(selector).accessibleMegaMenu("getDefaults"); 776 * @return {object} 777 * @memberof jQuery.fn.accessibleMegaMenu 778 * @instance 779 */ 780 getDefaults: function () { 781 return this._defaults; 782 }, 783 784 /** 785 * @desc Get any option set to plugin using its name (as string) 786 * @example $(selector).accessibleMegaMenu("getOption", some_option); 787 * @param {string} opt 788 * @return {string} 789 * @memberof jQuery.fn.accessibleMegaMenu 790 * @instance 791 */ 792 getOption: function (opt) { 793 return this.settings[opt]; 794 }, 795 796 /** 797 * @desc Get all options 798 * @example $(selector).accessibleMegaMenu("getAllOptions"); 799 * @return {object} 800 * @memberof jQuery.fn.accessibleMegaMenu 801 * @instance 802 */ 803 getAllOptions: function () { 804 return this.settings; 805 }, 806 807 /** 808 * @desc Set option 809 * @example $(selector).accessibleMegaMenu("setOption", "option_name", "option_value", reinitialize); 810 * @param {string} opt - Option name 811 * @param {string} val - Option value 812 * @param {boolean} [reinitialize] - boolean to re-initialize the menu. 813 * @memberof jQuery.fn.accessibleMegaMenu 814 * @instance 815 */ 816 setOption: function (opt, value, reinitialize) { 817 this.settings[opt] = value; 818 if (reinitialize) { 819 this.init(); 820 } 821 } 822 }; 823 }()); 824 825 /* lightweight plugin wrapper around the constructor, 826 to prevent against multiple instantiations */ 827 828 /** 829 * @class accessibleMegaMenu 830 * @memberOf jQuery.fn 831 * @classdesc Implements an accessible mega menu as a jQuery plugin. 832 * <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> 833 * <h3>Keyboard Accessibility</h3> 834 * <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> 835 * <h3>Screen Reader Accessibility</h3> 836 * <p>The accessible mega menu models its use of WAI-ARIA Roles, States, and Properties after those described
836in 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> 837 * @example <h4>HTML</h4><hr/> 838 <nav> 839 <ul class="nav-menu"> 840 <li class="nav-item"> 841 <a href="?movie">Movies</a> 842 <div class="sub-nav"> 843 <ul class="sub-nav-group"> 844 <li><a href="?movie&genre=0">Action &amp; Adventure</a></li> 845 <li><a href="?movie&genre=2">Children &amp; Family</a></li> 846 <li>&#8230;</li> 847 </ul> 848 <ul class="sub-nav-group"> 849 <li><a href="?movie&genre=7">Dramas</a></li> 850 <li><a href="?movie&genre=9">Foreign</a></li> 851 <li>&#8230;</li> 852 </ul> 853 <ul class="sub-nav-group"> 854 <li><a href="?movie&genre=14">Musicals</a></li> 855 <li><a href="?movie&genre=15">Romance</a></li> 856 <li>&#8230;</li> 857 </ul> 858 </div> 859 </li> 860 <li class="nav-item"> 861 <a href="?tv">TV Shows</a> 862 <div class="sub-nav"> 863 <ul class="sub-nav-group"> 864 <li><a href="?tv&genre=20">Classic TV</a></li> 865 <li><a href="?tv&genre=21">Crime TV</a></li> 866 <li>&#8230;</li> 867 </ul> 868 <ul class="sub-nav-group"> 869 <li><a href="?tv&genre=27">Reality TV</a></li> 870 <li><a href="?tv&genre=30">TV Action</a></li> 871 <li>&#8230;</li> 872 </ul> 873 <ul class="sub-nav-group"> 874 <li><a href="?tv&genre=33">TV Dramas</a></li> 875 <li><a href="?tv&genre=34">TV Horror</a></li> 876 <li>&#8230;</li> 877 </ul> 878 </div> 879 </li> 880 </ul> 881 </nav> 882 * @example <h4>CSS</h4><hr/> 883 /* Rudimentary mega menu CSS for demonstration */ 884 885 /* mega menu list */ 886 .nav-menu { 887 display: block; 888 position: relative; 889 list-style: none; 890 margin: 0; 891 padding: 0; 892 z-index: 15; 893} 894 895 /* a top level navigation item in the mega menu */ 896 .nav-item { 897 list-style: none; 898 display: inline-block; 899 padding: 0; 900 margin: 0; 901} 902 903 /* first descendant link within a top level navigation item */ 904 .nav-item > a { 905 position: relative; 906 display: inline-block; 907 padding: 0.5em 1em; 908 margin: 0 0 -1px 0; 909 border: 1px solid transparent; 910} 911 912 /* focus/open states of first descendant link within a top level 913 navigation item */ 914 .nav-item > a:focus, 915 .nav-item > a.open { 916 border: 1px solid #dedede; 917} 918 919 /* open state of first descendant link within a top level 920 navigation item */ 921 .nav-item > a.open { 922 background-color: #fff; 923 border-bottom: none; 924 z-index: 1; 925} 926 927 /* sub-navigation panel */ 928 .sub-nav { 929 position: absolute; 930 display: none; 931 top: 2.2em;
932 margin-top: -1px; 933 padding: 0.5em 1em; 934 border: 1px solid #dedede; 935 background-color: #fff; 936} 937 938 /* sub-navigation panel open state */ 939 .sub-nav.open { 940 display: block; 941} 942 943 /* list of items within sub-navigation panel */ 944 .sub-nav ul { 945 display: inline-block; 946 vertical-align: top; 947 margin: 0 1em 0 0; 948 padding: 0; 949} 950 951 /* list item within sub-navigation panel */ 952 .sub-nav li { 953 display: block; 954 list-style-type: none; 955 margin: 0; 956 padding: 0; 957} 958 * @example <h4>JavaScript</h4><hr/> 959 <!-- include jquery --> 960 <script src="http://code.jquery.com/jquery-1.10.1.min.js"></script> 961 962 <!-- include the jquery-accessibleMegaMenu plugin script --> 963 <script src="js/jquery-accessibleMegaMenu.js"></script> 964 965 <!-- initialize a selector as an accessibleMegaMenu --> 966 <script> 967 $("nav:first").accessibleMegaMenu({ 968 /* prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby */ 969 uuidPrefix: "accessible-megamenu", 970 971 /* css class used to define the megamenu styling */ 972 menuClass: "nav-menu", 973 974 /* css class for a top-level navigation item in the megamenu */ 975 topNavItemClass: "nav-item", 976 977 /* css class for a megamenu panel */ 978 panelClass: "sub-nav", 979 980 /* css class for a group of items within a megamenu panel */ 981 panelGroupClass: "sub-nav-group", 982 983 /* css class for the hover state */ 984 hoverClass: "hover", 985 986 /* css class for the focus state */ 987 focusClass: "focus", 988 989 /* css class for the open state */ 990 openClass: "open" 991 }); 992 </script> 993 * @param {object} [options] Mega Menu options 994 * @param {string} [options.uuidPrefix=accessible-megamenu] - Prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby 995 * @param {string} [options.menuClass=accessible-megamenu] - CSS class used to define the megamenu styling 996 * @param {string} [options.topNavItemClass=accessible-megamenu-top-nav-item] - CSS class for a top-level navigation item in the megamenu 997 * @param {string} [options.panelClass=accessible-megamenu-panel] - CSS class for a megamenu panel 998 * @param {string} [options.panelGroupClass=accessible-megamenu-panel-group] - CSS class for a group of items within a megamenu panel 999 * @param {string} [options.hoverClass=hover] - CSS class for the hover state 1000 * @param {string} [options.focusClass=focus] - CSS class for the focus state 1001 * @param {string} [options.openClass=open] - CSS class for the open state 1002 * @param {string} [options.openDelay=0] - Open delay when opening menu via mouseover 1003 * @param {string} [options.closeDelay=250] - Open delay when opening menu via mouseover 1004 */ 1005 $.fn[pluginName] = function (options) { 1006 return this.each(function () { 1007 if (!$.data(this, "plugin_" + pluginName)) { 1008 $.data(this, "plugin_" + pluginName, new $.fn[pluginName].Acce
1008ssibleMegaMenu(this, options)); 1009 } 1010 }); 1011 }; 1012 1013 $.fn[pluginName].AccessibleMegaMenu = AccessibleMegaMenu; 1014 1015 /* :focusable and :tabbable selectors from 1016 https://raw.github.com/jquery/jquery-ui/master/ui/jquery.ui.core.js */ 1017 1018 /** 1019 * @private 1020 */ 1021 function visible(element) { 1022 return $.expr.filters.visible(element) && !$(element).parents().addBack().filter(function () { 1023 return $.css(this, "visibility") === "hidden"; 1024 }).length; 1025 } 1026 1027 /** 1028 * @private 1029 */ 1030 function focusable(element, isTabIndexNotNaN) { 1031 var map, mapName, img, 1032 nodeName = element.nodeName.toLowerCase(); 1033 if ("area" === nodeName) { 1034 map = element.parentNode; 1035 mapName = map.name; 1036 if (!element.href || !mapName || map.nodeName.toLowerCase() !== "map") { 1037 return false; 1038 } 1039 img = $("img[usemap=#" + mapName + "]")[0]; 1040 return !!img && visible(img); 1041 } 1042 return (/input|select|textarea|button|object/.test(nodeName) ? !element.disabled : 1043 "a" === nodeName ? 1044 element.href || isTabIndexNotNaN : 1045 isTabIndexNotNaN) && 1046 // the element and all of its ancestors must be visible 1047 visible(element); 1048 } 1049 1050 $.extend($.expr[":"], { 1051 data: $.expr.createPseudo ? $.expr.createPseudo(function (dataName) { 1052 return function (elem) { 1053 return !!$.data(elem, dataName); 1054 }; 1055 }) : // support: jQuery <1.8 1056 function (elem, i, match) { 1057 return !!$.data(elem, match[3]); 1058 }, 1059 1060 focusable: function (element) { 1061 return focusable(element, !isNaN($.attr(element, "tabindex"))); 1062 }, 1063 1064 tabbable: function (element) { 1065 var tabIndex = $.attr(element, "tabindex"), 1066 isTabIndexNaN = isNaN(tabIndex); 1067 return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); 1068 } 1069 }); 1070}(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.