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 "aria-expanded": false, 744 "aria-hidden": true 745 }) 746 .addClass(settings.panelClass) 747 .not("[aria-labelledby]") 748 .attr("aria-labelledby", topnavitemlink.attr("id")); 749 } 750 }); 751 752 this.panels = menu.find("." + settings.panelClass); 753 754 menu.on("focusin.accessible-megamenu", ":focusable, ." + settings.panelClass, $.proxy(_focusInHandler, this)) 755 .on("focusout.accessible-megamenu", ":focusable, ." + settings.panelClass, $.proxy(_focusOutHandler, this))
756 .on("keydown.accessible-megamenu", $.proxy(_keyDownHandler, this)) 757 .on("mouseover.accessible-megamenu", $.proxy(_mouseOverHandler, this)) 758 .on("mouseout.accessible-megamenu", $.proxy(_mouseOutHandler, this)) 759 .on("mousedown.accessible-megamenu", $.proxy(_mouseDownHandler, this)); 760 761 if (isTouch) { 762 menu.on("touchstart.accessible-megamenu", $.proxy(_clickHandler, this)); 763 } 764 765 menu.find("hr").attr("role", "separator"); 766 767 if ($(document.activeElement).closest(menu).length) { 768 $(document.activeElement).trigger("focusin.accessible-megamenu"); 769 } 770 }, 771 772 /** 773 * @desc Get default values 774 * @example $(selector).accessibleMegaMenu("getDefaults"); 775 * @return {object} 776 * @memberof jQuery.fn.accessibleMegaMenu 777 * @instance 778 */ 779 getDefaults: function () { 780 return this._defaults; 781 }, 782 783 /** 784 * @desc Get any option set to plugin using its name (as string) 785 * @example $(selector).accessibleMegaMenu("getOption", some_option); 786 * @param {string} opt 787 * @return {string} 788 * @memberof jQuery.fn.accessibleMegaMenu 789 * @instance 790 */ 791 getOption: function (opt) { 792 return this.settings[opt]; 793 }, 794 795 /** 796 * @desc Get all options 797 * @example $(selector).accessibleMegaMenu("getAllOptions"); 798 * @return {object} 799 * @memberof jQuery.fn.accessibleMegaMenu 800 * @instance 801 */ 802 getAllOptions: function () { 803 return this.settings; 804 }, 805 806 /** 807 * @desc Set option 808 * @example $(selector).accessibleMegaMenu("setOption", "option_name", "option_value", reinitialize); 809 * @param {string} opt - Option name 810 * @param {string} val - Option value 811 * @param {boolean} [reinitialize] - boolean to re-initialize the menu. 812 * @memberof jQuery.fn.accessibleMegaMenu 813 * @instance 814 */ 815 setOption: function (opt, value, reinitialize) { 816 this.settings[opt] = value; 817 if (reinitialize) { 818 this.init(); 819 } 820 } 821 }; 822 }()); 823 824 /* lightweight plugin wrapper around the constructor, 825 to prevent against multiple instantiations */ 826 827 /** 828 * @class accessibleMegaMenu 829 * @memberOf jQuery.fn 830 * @classdesc Implements an accessible mega menu as a jQuery plugin. 831 * <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> 832 * <h3>Keyboard Accessibility</h3> 833 * <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> 834 * <h3>Screen Reader Accessibility</h3> 835 * <p>The accessible mega menu models its use of WAI-ARIA Roles, States, and Properties after those described
835in 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> 836 * @example <h4>HTML</h4><hr/> 837<nav> 838 <ul class="nav-menu"> 839 <li class="nav-item"> 840 <a href="?movie">Movies</a> 841 <div class="sub-nav"> 842 <ul class="sub-nav-group"> 843 <li><a href="?movie&genre=0">Action &amp; Adventure</a></li> 844 <li><a href="?movie&genre=2">Children &amp; Family</a></li> 845 <li>&#8230;</li> 846 </ul> 847 <ul class="sub-nav-group"> 848 <li><a href="?movie&genre=7">Dramas</a></li> 849 <li><a href="?movie&genre=9">Foreign</a></li> 850 <li>&#8230;</li> 851 </ul> 852 <ul class="sub-nav-group"> 853 <li><a href="?movie&genre=14">Musicals</a></li> 854 <li><a href="?movie&genre=15">Romance</a></li> 855 <li>&#8230;</li> 856 </ul> 857 </div> 858 </li> 859 <li class="nav-item"> 860 <a href="?tv">TV Shows</a> 861 <div class="sub-nav"> 862 <ul class="sub-nav-group"> 863 <li><a href="?tv&genre=20">Classic TV</a></li> 864 <li><a href="?tv&genre=21">Crime TV</a></li> 865 <li>&#8230;</li> 866 </ul> 867 <ul class="sub-nav-group"> 868 <li><a href="?tv&genre=27">Reality TV</a></li> 869 <li><a href="?tv&genre=30">TV Action</a></li> 870 <li>&#8230;</li> 871 </ul> 872 <ul class="sub-nav-group"> 873 <li><a href="?tv&genre=33">TV Dramas</a></li> 874 <li><a href="?tv&genre=34">TV Horror</a></li> 875 <li>&#8230;</li> 876 </ul> 877 </div> 878 </li> 879 </ul> 880</nav> 881 * @example <h4>CSS</h4><hr/> 882/* Rudimentary mega menu CSS for demonstration */ 883 884/* mega menu list */ 885.nav-menu { 886 display: block; 887 position: relative; 888 list-style: none; 889 margin: 0; 890 padding: 0; 891 z-index: 15; 892} 893 894/* a top level navigation item in the mega menu */ 895.nav-item { 896 list-style: none; 897 display: inline-block; 898 padding: 0; 899 margin: 0; 900} 901 902/* first descendant link within a top level navigation item */ 903.nav-item > a { 904 position: relative; 905 display: inline-block; 906 padding: 0.5em 1em; 907 margin: 0 0 -1px 0; 908 border: 1px solid transparent; 909} 910 911/* focus/open states of first descendant link within a top level 912 navigation item */ 913.nav-item > a:focus, 914.nav-item > a.open { 915 border: 1px solid #dedede; 916} 917 918/* open state of first descendant link within a top level 919 navigation item */ 920.nav-item > a.open { 921 background-color: #fff; 922 border-bottom: none; 923 z-index: 1; 924} 925 926/* sub-navigation panel */ 927.sub-nav { 928 position: absolute; 929 display: none; 930 top: 2.2em;
931 margin-top: -1px; 932 padding: 0.5em 1em; 933 border: 1px solid #dedede; 934 background-color: #fff; 935} 936 937/* sub-navigation panel open state */ 938.sub-nav.open { 939 display: block; 940} 941 942/* list of items within sub-navigation panel */ 943.sub-nav ul { 944 display: inline-block; 945 vertical-align: top; 946 margin: 0 1em 0 0; 947 padding: 0; 948} 949 950/* list item within sub-navigation panel */ 951.sub-nav li { 952 display: block; 953 list-style-type: none; 954 margin: 0; 955 padding: 0; 956} 957 * @example <h4>JavaScript</h4><hr/> 958<!-- include jquery --> 959<script src="http://code.jquery.com/jquery-1.10.1.min.js"></script> 960 961<!-- include the jquery-accessibleMegaMenu plugin script --> 962<script src="js/jquery-accessibleMegaMenu.js"></script> 963 964<!-- initialize a selector as an accessibleMegaMenu --> 965<script> 966 $("nav:first").accessibleMegaMenu({ 967 /* prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby */ 968 uuidPrefix: "accessible-megamenu", 969 970 /* css class used to define the megamenu styling */ 971 menuClass: "nav-menu", 972 973 /* css class for a top-level navigation item in the megamenu */ 974 topNavItemClass: "nav-item", 975 976 /* css class for a megamenu panel */ 977 panelClass: "sub-nav", 978 979 /* css class for a group of items within a megamenu panel */ 980 panelGroupClass: "sub-nav-group", 981 982 /* css class for the hover state */ 983 hoverClass: "hover", 984 985 /* css class for the focus state */ 986 focusClass: "focus", 987 988 /* css class for the open state */ 989 openClass: "open" 990 }); 991</script> 992 * @param {object} [options] Mega Menu options 993 * @param {string} [options.uuidPrefix=accessible-megamenu] - Prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby 994 * @param {string} [options.menuClass=accessible-megamenu] - CSS class used to define the megamenu styling 995 * @param {string} [options.topNavItemClass=accessible-megamenu-top-nav-item] - CSS class for a top-level navigation item in the megamenu 996 * @param {string} [options.panelClass=accessible-megamenu-panel] - CSS class for a megamenu panel 997 * @param {string} [options.panelGroupClass=accessible-megamenu-panel-group] - CSS class for a group of items within a megamenu panel 998 * @param {string} [options.hoverClass=hover] - CSS class for the hover state 999 * @param {string} [options.focusClass=focus] - CSS class for the focus state 1000 * @param {string} [options.openClass=open] - CSS class for the open state 1001 * @param {string} [options.openDelay=0] - Open delay when opening menu via mouseover 1002 * @param {string} [options.closeDelay=250] - Open delay when opening menu via mouseover 1003 */ 1004 $.fn[pluginName] = function (options) { 1005 return this.each(function () { 1006 if (!$.data(this, "plugin_" + pluginName)) { 1007 $.data(this, "plugin_" + pluginName, new $.fn[pluginName].Acce
1007ssibleMegaMenu(this, options)); 1008 } 1009 }); 1010 }; 1011 1012 $.fn[pluginName].AccessibleMegaMenu = AccessibleMegaMenu; 1013 1014 /* :focusable and :tabbable selectors from 1015 https://raw.github.com/jquery/jquery-ui/master/ui/jquery.ui.core.js */ 1016 1017 /** 1018 * @private 1019 */ 1020 function visible(element) { 1021 return $.expr.filters.visible(element) && !$(element).parents().addBack().filter(function () { 1022 return $.css(this, "visibility") === "hidden"; 1023 }).length; 1024 } 1025 1026 /** 1027 * @private 1028 */ 1029 function focusable(element, isTabIndexNotNaN) { 1030 var map, mapName, img, 1031 nodeName = element.nodeName.toLowerCase(); 1032 if ("area" === nodeName) { 1033 map = element.parentNode; 1034 mapName = map.name; 1035 if (!element.href || !mapName || map.nodeName.toLowerCase() !== "map") { 1036 return false; 1037 } 1038 img = $("img[usemap=#" + mapName + "]")[0]; 1039 return !!img && visible(img); 1040 } 1041 return (/input|select|textarea|button|object/.test(nodeName) ? !element.disabled : 1042 "a" === nodeName ? 1043 element.href || isTabIndexNotNaN : 1044 isTabIndexNotNaN) && 1045 // the element and all of its ancestors must be visible 1046 visible(element); 1047 } 1048 1049 $.extend($.expr[":"], { 1050 data: $.expr.createPseudo ? $.expr.createPseudo(function (dataName) { 1051 return function (elem) { 1052 return !!$.data(elem, dataName); 1053 }; 1054 }) : // support: jQuery <1.8 1055 function (elem, i, match) { 1056 return !!$.data(elem, match[3]); 1057 }, 1058 1059 focusable: function (element) { 1060 return focusable(element, !isNaN($.attr(element, "tabindex"))); 1061 }, 1062 1063 tabbable: function (element) { 1064 var tabIndex = $.attr(element, "tabindex"), 1065 isTabIndexNaN = isNaN(tabIndex); 1066 return (isTabIndexNaN || tabIndex >= 0) && focusable(element, !isTabIndexNaN); 1067 } 1068 }); 1069}(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.