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 openOnMouseover: true // default setting for whether menu should open on mouseover 62 }, 63 Keyboard = { 64 BACKSPACE: 8, 65 COMMA: 188, 66 DELETE: 46, 67 DOWN: 40, 68 END: 35, 69 ENTER: 13, 70 ESCAPE: 27, 71 HOME: 36, 72 LEFT: 37, 73 PAGE_DOWN: 34, 74 PAGE_UP: 33, 75 PERIOD: 190, 76 RIGHT: 39, 77 SPACE: 32, 78 TAB: 9, 79 UP: 38, 80 keyMap: { 81 48: "0", 82 49: "1", 83 50: "2", 84 51: "3", 85 52: "4", 86 53: "5", 87 54: "6", 88 55: "7", 89 56: "8", 90 57: "9", 91 59: ";", 92 65: "a", 93 66: "b", 94 67: "c", 95 68: "d", 96 69: "e", 97 70: "f", 98 71: "g", 99 72: "h", 100 73: "i", 101 74: "j", 102 75: "k", 103 76: "l", 104 77: "m", 105 78: "n", 106 79: "o", 107 80: "p", 108 81: "q", 109 82: "r", 110 83: "s", 111 84: "t", 112 85: "u", 113 86: "v", 114 87: "w", 115 88: "x", 116 89: "y", 117 90: "z", 118 96: "0", 119 97: "1", 120 98: "2", 121 99: "3", 122 100: "4", 123 101: "5", 124 102: "6", 125 103: "7", 126 104: "8", 127 105: "9", 128 190: "." 129 } 130 }; 131 /**
132 * @desc Creates a new accessible mega menu instance. 133 * @param {jquery} element 134 * @param {object} [options] Mega Menu options 135 * @param {string} [options.uuidPrefix=accessible-megamenu] - Prefix for generated unique id attributes, which are required to indicate aria-owns, aria-controls and aria-labelledby 136 * @param {string} [options.menuClass=accessible-megamenu] - CSS class used to define the megamenu styling 137 * @param {string} [options.topNavItemClass=accessible-megamenu-top-nav-item] - CSS class for a top-level navigation item in the megamenu 138 * @param {string} [options.panelClass=accessible-megamenu-panel] - CSS class for a megamenu panel 139 * @param {string} [options.panelGroupClass=accessible-megamenu-panel-group] - CSS class for a group of items within a megamenu panel 140 * @param {string} [options.hoverClass=hover] - CSS class for the hover state 141 * @param {string} [options.focusClass=focus] - CSS class for the focus state 142 * @param {string} [options.openClass=open] - CSS class for the open state 143 * @constructor 144 */ 145 function AccessibleMegaMenu(element, options) { 146 this.element = element; 147 148 // merge optional settings and defaults into settings 149 this.settings = $.extend({}, defaults, options); 150 151 this._defaults = defaults; 152 this._name = pluginName; 153 154 this.mouseTimeoutID = null; 155 this.focusTimeoutID = null; 156 this.mouseFocused = false; 157 this.justFocused = false; 158 159 this.init(); 160 } 161 162 AccessibleMegaMenu.prototype = (function () { 163 164 /* private attributes and methods ------------------------ */ 165 var uuid = 0, 166 keydownTimeoutDuration = 1000, 167 keydownSearchString = "", 168 isTouch = typeof window.hasOwnProperty === "function" && !!window.hasOwnProperty("ontouchstart"), 169 _getPlugin, 170 _addUniqueId, 171 _togglePanel, 172 _clickHandler, 173 _clickOutsideHandler, 174 _DOMAttrModifiedHandler, 175 _focusInHandler, 176 _focusOutHandler, 177 _keyDownHandler, 178 _mouseDownHandler, 179 _mouseOverHandler, 180 _mouseOutHandler, 181 _toggleExpandedEventHandlers; 182 183 /** 184 * @name jQuery.fn.accessibleMegaMenu~_getPlugin 185 * @desc Returns the parent accessibleMegaMenu instance for a given element 186 * @param {jQuery} element 187 * @memberof jQuery.fn.accessibleMegaMenu 188 * @inner 189 * @private 190 */ 191 _getPlugin = function (element) { 192 return $(element).closest(':data(plugin_' + pluginName + ')').data("plugin_" + pluginName); 193 }; 194 195 /** 196 * @name jQuery.fn.accessibleMegaMenu~_addUniqueId 197 * @desc Adds a unique id and element. 198 * The id string starts with the 199 * string defined in settings.uuidPrefix. 200 * @param {jQuery} element 201 * @memberof jQuery.fn.accessibleMegaMenu 202 * @inner 203 * @private 204 */ 205 _addUniqueId = function (element) { 206 element = $(element); 207 var settings = this.settings; 208 if (!element.attr("id")) { 209 element.attr("id", settings.uuidPrefix + "-" + new Date().getTime() + "-" + (++uuid)); 210 } 211 }; 212 213 /** 214 * @name jQuery.fn.accessibleMegaMenu~_togglePanel 215 * @desc Toggle the display of mega menu panels in response to an event. 216 * The optional boolean value 'hide' forces all panels to hide. 217 * @param {event} event 218 * @param {Boolean} [hide] Hide all mega menu panels when true 219 * @memberof jQuery.fn.accessibleMegaMenu 220 * @inner 221 * @private 222 */ 223 _togglePanel = function (event, hide) { 224 var target = $(event.target), 225 that = this, 226 settings = this.settings, 227 menu = this.menu, 228 topli = target.closest('.' + settings.topNavItemClass), 229 panel = target.hasClass(settings.panelClass) ? target : target.closest('.' + settings.panelClass), 230 newfocus; 231 232 _toggleExpandedEventHandlers.call(this, true); 233 234 if (hide) { 235 topli = menu.find('.' + settings.topNavItemClass + ' .' + settings.openClass + ':first').closest('.' + settings.topNavItemClass); 236 if (!(topli.is(event.relatedTarget) || topli.has(event.relatedTarget).length > 0)) { 237 if ((event.type === 'mouseout' || event.type === 'focusout') && topli.has(document.activeElement).length > 0) { 238 return; 239 } 240 topli.find('[aria-expanded]') 241 .attr('aria-expanded', 'false') 242 .removeClass(settings.openClass) 243 .filter('.' + settings.panelClass) 244 .attr('aria-hidden', 'true'); 245 if ((event.type === 'keydown' && event.keyCode === Keyboard.ESCAPE) || event.type === 'DOMAttrModified') { 246 newfocus = topli.find(':tabbable:first'); 247 setTimeout(function () { 248 menu.find('[aria-expanded].' + that.settings.panelClass).off('DOMAttrModified.accessible-megamenu'); 249 newfocus.focus(); 250 that.justFocused = false;
251 }, 99); 252 } 253 } else if (topli.length === 0) { 254 menu.find('[aria-expanded=true]') 255 .attr('aria-expanded', 'false') 256 .removeClass(settings.openClass) 257 .filter('.' + settings.panelClass) 258 .attr('aria-hidden', 'true'); 259 } 260 } else { 261 clearTimeout(that.focusTimeoutID); 262 topli.siblings() 263 .find('[aria-expanded]') 264 .attr('aria-expanded', 'false') 265 .removeClass(settings.openClass) 266 .filter('.' + settings.panelClass) 267 .attr('aria-hidden', 'true'); 268 topli.find('[aria-expanded]') 269 .attr('aria-expanded', 'true') 270 .addClass(settings.openClass) 271 .filter('.' + settings.panelClass) 272 .attr('aria-hidden', 'false'); 273 if (event.type === 'mouseover' && target.is(':tabbable') && topli.length === 1 && panel.length === 0 && menu.has(document.activeElement).length > 0) { 274 target.focus(); 275 that.justFocused = false; 276 } 277 278 _toggleExpandedEventHandlers.call(that); 279 } 280 }; 281 282 /** 283 * @name jQuery.fn.accessibleMegaMenu~_clickHandler 284 * @desc Handle click event on mega menu item 285 * @param {event} Event object 286 * @memberof jQuery.fn.accessibleMegaMenu 287 * @inner 288 * @private 289 */ 290 _clickHandler = function (event) { 291 var target = $(event.target).closest(':tabbable'), 292 topli = target.closest('.' + this.settings.topNavItemClass), 293 panel = target.closest('.' + this.settings.panelClass); 294 if (topli.length === 1 295 && panel.length === 0 296 && topli.find('.' + this.settings.panelClass).length === 1) { 297 if (!target.hasClass(this.settings.openClass)) { 298 event.preventDefault(); 299 event.stopPropagation(); 300 _togglePanel.call(this, event); 301 this.justFocused = false; 302 } else { 303 if (this.justFocused) { 304 event.preventDefault(); 305 event.stopPropagation(); 306 this.justFocused = false; 307 } else if (isTouch) { 308 event.preventDefault(); 309 event.stopPropagation(); 310 _togglePanel.call(this, event, target.hasClass(this.settings.openClass)); 311 } 312 } 313 } 314 }; 315 316 /** 317 * @name jQuery.fn.accessibleMegaMenu~_clickOutsideHandler 318 * @desc Handle click event outside of a the megamenu 319 * @param {event} Event object 320 * @memberof jQuery.fn.accessibleMegaMenu 321 * @inner 322 * @private 323 */ 324 _clickOutsideHandler = function (event) { 325 if ($(event.target).closest(this.menu).length === 0) { 326 event.preventDefault(); 327 event.stopPropagation(); 328 _togglePanel.call(this, event, true); 329 } 330 }; 331 332 /** 333 * @name jQuery.fn.accessibleMegaMenu~_DOMAttrModifiedHandler 334 * @desc Handle DOMAttrModified event on panel to respond to Windows 8 Narrator ExpandCollapse pattern 335 * @param {event} Event object 336 * @memberof jQuery.fn.accessibleMegaMenu 337 * @inner 338 * @private 339 */ 340 _DOMAttrModifiedHandler = function (event) { 341 if (event.originalEvent.attrName === 'aria-expanded' 342 && event.originalEvent.newValue === 'false' 343 && $(event.target).hasClass(this.settings.openClass)) { 344 event.preventDefault(); 345 event.stopPropagation(); 346 _togglePanel.call(this, event, true); 347 } 348 }; 349 350 /** 351 * @name jQuery.fn.accessibleMegaMenu~_focusInHandler
352 * @desc Handle focusin event on mega menu item. 353 * @param {event} Event object 354 * @memberof jQuery.fn.accessibleMegaMenu 355 * @inner 356 * @private 357 */ 358 _focusInHandler = function (event) { 359 clearTimeout(this.focusTimeoutID); 360 var target = $(event.target), 361 panel = target.closest('.' + this.settings.panelClass); 362 target 363 .addClass(this.settings.focusClass) 364 .on('click.accessible-megamenu', $.proxy(_clickHandler, this)); 365 this.justFocused = !this.mouseFocused; 366 this.mouseFocused = false; 367 if (this.panels.not(panel).filter('.' + this.settings.openClass).length) { 368 _togglePanel.call(this, event); 369 } 370 }; 371 372 /** 373 * @name jQuery.fn.accessibleMegaMenu~_focusOutHandler 374 * @desc Handle focusout event on mega menu item. 375 * @param {event} Event object 376 * @memberof jQuery.fn.accessibleMegaMenu 377 * @inner 378 * @private 379 */ 380 _focusOutHandler = function (event) { 381 this.justFocused = false; 382 var that = this, 383 target = $(event.target), 384 topli = target.closest('.' + this.settings.topNavItemClass), 385 keepOpen = false; 386 target 387 .removeClass(this.settings.focusClass) 388 .off('click.accessible-megamenu'); 389 390 if (window.cvox) { 391 // If ChromeVox is running... 392 that.focusTimeoutID = setTimeout(function () { 393 window.cvox.Api.getCurrentNode(function (node) { 394 if (topli.has(node).length) { 395 // and the current node being voiced is in 396 // the mega menu, clearTimeout, 397 // so the panel stays open. 398 clearTimeout(that.focusTimeoutID); 399 } else { 400 that.focusTimeoutID = setTimeout(function (scope, event, hide) { 401 _togglePanel.call(scope, event, hide); 402 }, 275, that, event, true); 403 } 404 }); 405 }, 25); 406 } else { 407 that.focusTimeoutID = setTimeout(function () { 408 _togglePanel.call(that, event, true); 409 }, 300); 410 } 411 }; 412 413 /** 414 * @name jQuery.fn.accessibleMegaMenu~_keyDownHandler 415 * @desc Handle keydown event on mega menu. 416 * @param {event} Event object 417 * @memberof jQuery.fn.accessibleMegaMenu 418 * @inner 419 * @private 420 */ 421 _keyDownHandler = function (event) { 422 var that = (this.constructor === AccessibleMegaMenu) ? this : _getPlugin(this), // determine the AccessibleMegaMenu plugin instance 423 settings = that.settings, 424 target = $($(this).is('.' + settings.hoverClass + ':tabbable') ? this : event.target), // if the element is hovered the target is this, otherwise, its the focused element 425 menu = that.menu, 426 topnavitems = that.topnavitems, 427 topli = target.closest('.' + settings.topNavItemClass), 428 tabbables = menu.find(':tabbable'), 429 panel = target.hasClass(settings.panelClass) ? target : target.closest('.' + settings.panelClass), 430 panelGroups = panel.find('.' + settings.panelGroupClass), 431 currentPanelGroup = target.closest('.' + settings.panelGroupClass), 432 next, 433 keycode = event.keyCode || event.which, 434 start, 435 i, 436 o, 437 label, 438 found = false, 439 newString = Keyboard.keyMap[event.keyCode] || '', 440 regex, 441 isTopNavItem = (topli.length === 1 && panel.length === 0); 442
443 if (target.is("input:focus, select:focus, textarea:focus, button:focus")) { 444 // if the event target is a form element we should handle keydown normally 445 return; 446 } 447 448 if (target.is('.' + settings.hoverClass + ':tabbable')) { 449 $('html').off('keydown.accessible-megamenu'); 450 } 451 452 switch (keycode) { 453 case Keyboard.ESCAPE: 454 _togglePanel.call(that, event, true); 455 break; 456 case Keyboard.DOWN: 457 event.preventDefault(); 458 if (isTopNavItem) { 459 _togglePanel.call(that, event); 460 found = (topli.find('.' + settings.panelClass + ' :tabbable:first').focus().length === 1); 461 } else { 462 found = (tabbables.filter(':gt(' + tabbables.index(target) + '):first').focus().length === 1); 463 } 464 465 if (!found && window.opera && opera.toString() === "[object Opera]" && (event.ctrlKey || event.metaKey)) { 466 tabbables = $(':tabbable'); 467 i = tabbables.index(target); 468 found = ($(':tabbable:gt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 469 } 470 break; 471 case Keyboard.UP: 472 event.preventDefault(); 473 if (isTopNavItem && target.hasClass(settings.openClass)) { 474 _togglePanel.call(that, event, true); 475 next = topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last'); 476 if (next.children('.' + settings.panelClass).length) { 477 found = (next.children() 478 .attr('aria-expanded', 'true') 479 .addClass(settings.openClass) 480 .filter('.' + settings.panelClass) 481 .attr('aria-hidden', 'false') 482 .find(':tabbable:last') 483 .focus() === 1); 484 } 485 } else if (!isTopNavItem) { 486 found = (tabbables.filter(':lt(' + tabbables.index(target) + '):last').focus().length === 1); 487 } 488 489 if (!found && window.opera && opera.toString() === "[object Opera]" && (event.ctrlKey || event.metaKey)) { 490 tabbables = $(':tabbable'); 491 i = tabbables.index(target); 492 found = ($(':tabbable:lt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 493 } 494 break; 495 case Keyboard.RIGHT: 496 event.preventDefault(); 497 if (isTopNavItem) { 498 found = (topnavitems.filter(':gt(' + topnavitems.index(topli) + '):first').find(':tabbable:first').focus().length === 1); 499 } else { 500 if (panelGroups.length && currentPanelGroup.length) { 501 // if the current panel contains panel groups, and we are able to focus the first tabbable element of the next panel group 502 found = (panelGroups.filter(':gt(' + panelGroups.index(currentPanelGroup) + '):first').find(':tabbable:first').focus().length === 1); 503 } 504 505 if (!found) { 506 found = (topli.find(':tabbable:first').focus().length === 1); 507 } 508 } 509 break; 510 case Keyboard.LEFT: 511 event.preventDefault(); 512 if (isTopNavItem) { 513 found = (topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last').find(':tabbable:first').focus().length === 1); 514 } else { 515 if (panelGroups.length && currentPanelGroup.length) { 516 // if the current panel contains panel groups, and we are able to focus the first tabbable element of the previous panel group 517 found = (panelGroups.filter(':lt(' + panelGroups.index(currentPanelGroup) + '):last').find(':tabbable:first').focus().length === 1); 518 } 519 520 if (!found) { 521 found = (topli.find(':tabbable:first').focus().length === 1); 522 } 523 } 524 break; 525 case Keyboard.TAB: 526 i = tabbables.index(target); 527 if (event.shiftKey && isTopNavItem && target.hasClass(settings.openClass)) { 528 _togglePanel.call(that, event, true); 529 next = topnavitems.filter(':lt(' + topnavitems.index(topli) + '):last'); 530 if (next.children('.' + settings.panelClass).length) { 531 found = next.children() 532 .attr('aria-expanded', 'true') 533 .addClass(settings.openClass) 534 .filter('.' + settings.panelClass) 535 .attr('aria-hidden', 'false') 536 .find(':tabbable:last') 537 .focus(); 538 } 539 } else if (event.shiftKey && i > 0) { 540 found = (tabbables.filter(':lt(' + i + '):last').focus().length === 1); 541 } else if (!event.shiftKey && i < tabbables.length - 1) { 542 found = (tabbables.filter(':gt(' + i + '):first').focus().length === 1); 543 } else if (window.opera && opera.toString() === "[object Opera]") { 544 tabbables = $(':tabbable'); 545 i = tabbables.index(target); 546 if (event.shiftKey) { 547 found = ($(':tabbable:lt(' + $(':tabbable').index(target) + '):last').focus().length === 1); 548 } else { 549 found = ($(':tabbable:gt(' + $(':tabbable').index(target) + '):first').focus().length === 1); 550 } 551 } 552 553 if (found) { 554 event.preventDefault(); 555 } 556 break; 557 case Keyboard.SPACE: 558 if (isTopNavItem) { 559 event.preventDefault(); 560 _clickHandler.call(that, event); 561 } else { 562 return true; 563 } 564 break; 565 case Keyboard.ENTER: 566 return true; 567 break; 568 default: 569 // alphanumeric filter 570 clearTimeout(this.keydownTimeoutID); 571 572 keydownSearchString += newString !== keydownSearchString ? newString : ''; 573 574 if (keydownSearchString.length === 0) { 575 return; 576 } 577 578 this.keydownTimeoutID = setTimeout(function () { 579 keydownSearchString = ''; 580 }, keydownTimeoutDuration); 581 582 if (isTopNavItem && !target.hasClass(settings.openClass)) { 583 tabbables = tabbables.filter(':not(.' + settings.panelClass + ' :tabbable)'); 584 } else { 585 tabbables = topli.find(':tabbable'); 586 } 587 588 if (event.shiftKey) { 589 tabbables = $(tabbables.get() 590 .reverse()); 591 } 592 593 for (i = 0; i < tabbables.length; i++) { 594 o = tabbables.eq(i); 595 if (o.is(target)) { 596 start = (keydownSearchString.length === 1) ? i + 1 : i; 597 break; 598 } 599 } 600 601 regex = new RegExp('^' + keydownSearchString.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&'), 'i'); 602 603 for (i = start; i < tabbables.length; i++) { 604 o = tabbables.eq(i); 605 label = $.trim(o.text()); 606 if (regex.test(label)) { 607 found = true; 608 o.focus(); 609 break; 610 } 611 } 612 if (!found) { 613 for (i = 0; i < start; i++) { 614 o = tabbables.eq(i); 615 label = $.trim(o.text()); 616 if (regex.test(label)) { 617 o.focus(); 618 break; 619 } 620 } 621 } 622 break; 623 } 624 that.justFocused = false;
625 }; 626 627 /** 628 * @name jQuery.fn.accessibleMegaMenu~_mouseDownHandler 629 * @desc Handle mousedown event on mega menu. 630 * @param {event} Event object 631 * @memberof accessibleMegaMenu 632 * @inner 633 * @private 634 */ 635 _mouseDownHandler = function (event) { 636 if ($(event.target).is(this.settings.panelClass) || $(event.target).closest(":focusable").length) { 637 this.mouseFocused = true; 638 } 639 clearTimeout(this.mouseTimeoutID); 640 this.mouseTimeoutID = setTimeout(function () { 641 clearTimeout(this.focusTimeoutID); 642 }, 1); 643 }; 644 645 /** 646 * @name jQuery.fn.accessibleMegaMenu~_mouseOverHandler 647 * @desc Handle mouseover event on mega menu. 648 * @param {event} Event object 649 * @memberof jQuery.fn.accessibleMegaMenu 650 * @inner 651 * @private 652 */ 653 _mouseOverHandler = function (event) { 654 clearTimeout(this.mouseTimeoutID); 655 var that = this; 656 this.mouseTimeoutID = setTimeout(function () { 657 $(event.target).addClass(that.settings.hoverClass); 658 _togglePanel.call(that, event); 659 if ($(event.target).is(':tabbable')) { 660 $('html').on('keydown.accessible-megamenu', $.proxy(_keyDownHandler, event.target)); 661 } 662 }, this.settings.openDelay); 663 }; 664 665 /** 666 * @name jQuery.fn.accessibleMegaMenu~_mouseOutHandler 667 * @desc Handle mouseout event on mega menu. 668 * @param {event} Event object 669 * @memberof jQuery.fn.accessibleMegaMenu 670 * @inner 671 * @private 672 */ 673 _mouseOutHandler = function (event) { 674 clearTimeout(this.mouseTimeoutID); 675 var that = this; 676 $(event.target) 677 .removeClass(that.settings.hoverClass); 678 679 that.mouseTimeoutID = setTimeout(function () { 680 _togglePanel.call(that, event, true); 681 }, this.settings.closeDelay); 682 if ($(event.target).is(':tabbable')) { 683 $('html').off('keydown.accessible-megamenu'); 684 } 685 }; 686 687 _toggleExpandedEventHandlers = function (hide) { 688 var menu = this.menu; 689 if (hide) { 690 $('html').off('mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-accessible-megamenu, pointerup.outside-accessible-megamenu'); 691 692 menu.find('[aria-expanded].' + this.settings.panelClass).off('DOMAttrModified.accessible-megamenu'); 693 } else { 694 $('html').on('mouseup.outside-accessible-megamenu, touchend.outside-accessible-megamenu, mspointerup.outside-accessible-megamenu, pointerup.outside-accessible-megamenu', $.proxy(_clickOutsideHandler, this)); 695 696 /* Narrator in Windows 8 automatically toggles the aria-expanded property on double tap or click. 697 To respond to the change to collapse the panel, we must add a listener for a DOMAttrModified event. */ 698 menu.find('[aria-expanded=true].' + this.settings.panelClass).on('DOMAttrModified.accessible-megamenu', $.proxy(_DOMAttrModifiedHandler, this)); 699 } 700 }; 701 702 /* public attributes and methods ------------------------- */ 703 return { 704 constructor: AccessibleMegaMenu, 705 706 /** 707 * @lends jQuery.fn.accessibleMegaMenu 708 * @desc Initializes an instance of the accessibleMegaMenu plugins 709 * @memberof jQuery.fn.accessibleMegaMenu 710 * @instance 711 */ 712 init: function () { 713 var settings = this.settings, 714 nav = $(this.element), 715 menu = nav.children().first(), 716 topnavitems = menu.children(); 717 this.start(settings, nav, menu, topnavitems); 718 }, 719 720 start: function(settings, nav, menu, topnavitems) { 721 var that = this; 722 this.settings = settings; 723 this.menu = menu; 724 this.topnavitems = topnavitems; 725 726 nav.attr("role", "menubar"); 727 menu.addClass(settings.menuClass); 728 topnavitems.each(function (i, topnavitem) { 729 var topnavitemlink, topnavitempanel; 730 topnavitem = $(topnavitem); 731 topnavitem.addClass(settings.topNavItemClass); 732 topnavitemlink = topnavitem.find(":tabbable:first"); 733 topnavitempanel = topnavitem.children(":not(:tabbable):last"); 734 _addUniqueId.call(that, topnavitemlink); 735 if (topnavitempanel.length) { 736 _addUniqueId.call(that, topnavitempanel); 737 topnavitemlink.attr({ 738 // "aria-haspopup": true, 739 "aria-controls": topnavitempanel.attr("id"), 740 "aria-expanded": false 741 }); 742 743 topnavitempanel.attr({ 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 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
vendor: 2,162 bytes, lines 1007-1073
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)); 1070 1071 1072 1073
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.