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