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