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